mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 23:12:22 +02:00
merge: reconcile Wave 1.1 with post-PR40 lab
Merge canonical lab 9557b8d5909eb4a885c3bf49e19a65dd904f8c1d exactly once. Retain invocation journal ownership and lineage, provider terminal ordering, teacher handoff, framed DONE handling, and canonical authority/Ajax routing. Combine dynamic dispatch receipts with lab policy forwarding. Adapt native shell/patch evidence, explicit TUI verifiers, and artifact recovery presentation. Refresh generated configuration source links and strengthen adapter regressions. Validation: focused 2118 passed; Wave 1.1 script 2291 passed; broad runtime 5649 passed; full pytest 11581 passed, 53 skipped, 2 xfailed, 6 subtests passed. Compileall 1689 Python files; syntax 279 JS and 82 MJS files; diff and conflict-marker checks passed.
This commit is contained in:
+131
-17
@@ -27,7 +27,7 @@ from datetime import date, datetime, timedelta
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator, Dict, Iterable, List, Mapping, Optional, Sequence, Set
|
||||
from urllib.parse import parse_qs, parse_qsl, quote, unquote, urlparse
|
||||
from urllib.parse import parse_qs, parse_qsl, quote, unquote, urlencode, urlparse
|
||||
|
||||
from src.llm_core import (
|
||||
dedupe_model_candidates,
|
||||
@@ -4663,7 +4663,7 @@ def _memory_list_summary_from_tool_output(raw: str, max_items: int = 20) -> str:
|
||||
# memory in an invisible chat payload turned a simple list into a huge
|
||||
# terminal SSE event and copied private text into chat history.
|
||||
items.append(
|
||||
f"...and {remaining} more saved memories. Open Memory to browse all."
|
||||
f"...and {remaining} more saved memories. [Open Memory to browse all](#memory)."
|
||||
)
|
||||
return "\n".join([header, *items])
|
||||
|
||||
@@ -7486,7 +7486,7 @@ Or with JSON for fresh news:
|
||||
```web_search
|
||||
{"query": "<your query>", "time_filter": "day"}
|
||||
```
|
||||
Search the web for a SINGLE quick fact/lookup mid-task. For news / "today" / "latest" queries, pass `time_filter` ("day", "week", "month", or "year"). NOT for "research X" / "do research on X" / "look into X" requests — those mean a multi-source DEEP RESEARCH job: use `trigger_research` instead (it runs in the Deep Research sidebar and produces a full report). web_search = one quick query; trigger_research = a researched report.
|
||||
Search the web for a SINGLE quick fact/lookup mid-task. For recently published news/articles, pass `time_filter` ("day", "week", "month", or "year"); do not use a publication filter for current weather, prices, or other current facts. For weather, prefer `get_weather`. NOT for "research X" / "do research on X" / "look into X" requests — those mean a multi-source DEEP RESEARCH job: use `trigger_research` instead (it runs in the Deep Research sidebar and produces a full report). web_search = one quick query; trigger_research = a researched report.
|
||||
Choose the `query` yourself from the user's full request and recent conversation context. If the latest user message is only "can you search", "look it up", or similar, search for the prior topic, not the literal follow-up phrase.
|
||||
If this `web_search` tool section is visible, search is available. Do NOT tell the user web/search tools are unavailable.
|
||||
For products, hardware, software, launches, and releases, distinguish announcement date from release/ship/availability date. Do not call an announced future product "current" or "available" unless the evidence says it is shipping/available now.
|
||||
@@ -7498,6 +7498,12 @@ Use this instead of `bash`, `curl`, `python`, `requests`, scraping code, or brow
|
||||
```
|
||||
Fetch and read the text content of a SPECIFIC URL the user names (e.g. "check example.com", "what does this page say <url>"). A bare domain like `example.com` works (defaults to https). Use this when you already have a concrete URL. For open-ended lookups use `web_search`, and for "research X" jobs use `trigger_research`.""",
|
||||
|
||||
"get_weather": """\
|
||||
```get_weather
|
||||
{"location": "Tokyo, Japan"}
|
||||
```
|
||||
Get current conditions and a three-day forecast using Open-Meteo. Use this for weather questions before searching the web. No API key is required; include the returned source and local observation time in the answer.""",
|
||||
|
||||
"private_browser": """\
|
||||
```private_browser
|
||||
{"action": "open", "url": "https://example.com"}
|
||||
@@ -9168,16 +9174,8 @@ def _failed_tool_round_limit(
|
||||
|
||||
def _tui_python_runner_setup() -> str:
|
||||
"""Select the workspace interpreter, including a primary checkout venv."""
|
||||
|
||||
return (
|
||||
"runner=''; "
|
||||
"if [ -x .venv/bin/python ]; then runner=.venv/bin/python; "
|
||||
"elif [ -x venv/bin/python ]; then runner=venv/bin/python; "
|
||||
"elif git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) "
|
||||
"&& [ -x \"$(dirname \"$git_common\")/.venv/bin/python\" ]; then "
|
||||
"runner=\"$(dirname \"$git_common\")/.venv/bin/python\"; "
|
||||
"else runner=python; fi; "
|
||||
)
|
||||
from src.agent_runtime.identity import TUI_PYTHON_RUNNER_SETUP
|
||||
return TUI_PYTHON_RUNNER_SETUP
|
||||
|
||||
|
||||
def _tui_local_test_runner_command(*, full: bool = False) -> str:
|
||||
@@ -16645,6 +16643,20 @@ def _private_browser_blocked_by_bot_check(result: Any) -> bool:
|
||||
))
|
||||
|
||||
|
||||
def _should_retry_empty_search_in_browser(
|
||||
result: Any, disabled_tools: Set[str], tool_policy: Optional[ToolPolicy],
|
||||
already_tried: bool,
|
||||
) -> bool:
|
||||
"""Only promote an empty search to the browser when that tool is allowed."""
|
||||
return bool(
|
||||
isinstance(result, dict)
|
||||
and result.get("evidence_status") == "empty"
|
||||
and not already_tried
|
||||
and "private_browser" not in disabled_tools
|
||||
and not (tool_policy and tool_policy.blocks("private_browser"))
|
||||
)
|
||||
|
||||
|
||||
def _has_recent_web_tool_context(messages: List[Dict], *, max_messages: int = 6) -> bool:
|
||||
"""Return true when the latest turn follows recent public-web tool output."""
|
||||
seen_latest_user = False
|
||||
@@ -16729,6 +16741,18 @@ _WEATHER_CONTEXT_RE = re.compile(
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_WEATHER_TOOL_REQUEST_RE = re.compile(
|
||||
r"\b(?:weather|forecast|temperature|precipitation|humidity|"
|
||||
r"rain(?:ing|y)?|showers?|snow(?:ing|fall)?|wind\s+speed|uv\s+index)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_WEATHER_FOLLOWUP_TIME_RE = re.compile(
|
||||
r"\b(?:tomorrow|tmrw|tmr|today|tonight|weekend|next week|later|"
|
||||
r"status|update|how about|what about|same place)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_EXPLICIT_COOKBOOK_STATUS_RE = re.compile(
|
||||
r"\b(?:model|models|server|servers|serve|serving|served|endpoint|endpoints|"
|
||||
r"download|downloads|downloading|gpu|gpus|vllm|sglang|ollama|llama\.?cpp|"
|
||||
@@ -16808,7 +16832,10 @@ def _looks_like_contextual_weather_status_followup(messages: List[Dict], latest:
|
||||
return False
|
||||
if _EXPLICIT_COOKBOOK_STATUS_RE.search(value):
|
||||
return False
|
||||
if not _CONTEXTUAL_STATUS_FOLLOWUP_RE.search(value):
|
||||
if not (
|
||||
_CONTEXTUAL_STATUS_FOLLOWUP_RE.search(value)
|
||||
or _WEATHER_FOLLOWUP_TIME_RE.search(value)
|
||||
):
|
||||
return False
|
||||
|
||||
latest_clean = value.lower()
|
||||
@@ -16836,6 +16863,28 @@ def _looks_like_contextual_weather_status_followup(messages: List[Dict], latest:
|
||||
return False
|
||||
|
||||
|
||||
def _weather_tool_relevant(messages: List[Dict], latest: str) -> bool:
|
||||
"""Offer weather data for direct requests and short weather follow-ups."""
|
||||
if _WEATHER_TOOL_REQUEST_RE.search(latest or "") or "get_weather" in (latest or "").lower():
|
||||
return True
|
||||
value = str(latest or "").strip()
|
||||
if len(value.split()) > 8 or not _WEATHER_FOLLOWUP_TIME_RE.search(value):
|
||||
return False
|
||||
for message in reversed(messages or []):
|
||||
if not isinstance(message, dict) or message.get("role") != "user":
|
||||
continue
|
||||
prior = _message_content_text(message).strip()
|
||||
if prior == value:
|
||||
continue
|
||||
if _WEATHER_TOOL_REQUEST_RE.search(prior):
|
||||
return True
|
||||
# A follow-up can refer to an assistant's forecast after the latest
|
||||
# user message has been omitted from a compacted message window.
|
||||
break
|
||||
return _looks_like_contextual_weather_status_followup(messages, latest)
|
||||
return False
|
||||
|
||||
|
||||
def _web_search_assistant_context_text(messages: List[Dict], last_user: str) -> str:
|
||||
"""Recover public topic context from a recent assistant answer."""
|
||||
latest_clean = str(last_user or "").strip()
|
||||
@@ -22539,6 +22588,10 @@ async def stream_agent_loop(
|
||||
_ody_doc_stream_create_mode,
|
||||
_ody_general_no_tool_mode,
|
||||
) = _route_finetune_modes(model)
|
||||
if not _weather_tool_relevant(messages, _last_user):
|
||||
# A full native-tool surface normally advertises every authorized
|
||||
# schema. Weather is situational; omit it on unrelated turns too.
|
||||
disabled_tools.add("get_weather")
|
||||
_web_fetch_needs_private_browser = False
|
||||
_private_browser_needs_static_fallback = False
|
||||
_private_browser_store_handoff_done = False
|
||||
@@ -23712,7 +23765,7 @@ async def stream_agent_loop(
|
||||
if _contextual_weather_status_followup and not guide_only:
|
||||
_prepend_agent_directive(
|
||||
route_messages,
|
||||
"The user's short status/update question refers to the previous weather or forecast topic in this chat. Do not answer with Cookbook/model-serving/download status unless the user explicitly mentions models, servers, downloads, GPUs, or Cookbook. Use web_search/web_fetch if current weather evidence is needed.",
|
||||
"The user's short follow-up refers to the previous weather or forecast topic and location in this chat. Prefer get_weather for current forecast data, including tomorrow; do not ask for a location already established in the conversation. Do not answer with Cookbook/model-serving/download status unless the user explicitly mentions models, servers, downloads, GPUs, or Cookbook.",
|
||||
)
|
||||
if _map_browser_turn and not guide_only:
|
||||
_prepend_agent_directive(
|
||||
@@ -24104,6 +24157,7 @@ async def stream_agent_loop(
|
||||
# model is composing the answer.
|
||||
_web_search_completed = False
|
||||
_last_web_search_output = ""
|
||||
_empty_search_browser_fallback_done = False
|
||||
_last_web_retry_round_response = ""
|
||||
_web_fetch_pagination_counts: collections.Counter = collections.Counter()
|
||||
_compact_memory_list_turn = False
|
||||
@@ -24360,7 +24414,12 @@ async def stream_agent_loop(
|
||||
# their offerings in the prompt, not as native function schemas.
|
||||
if guide_only or not route_state["is_api_model"]:
|
||||
return []
|
||||
return _apply_tool_surface_to_schemas(turn_contract.schemas(), tool_surface)
|
||||
contract_schemas = [
|
||||
schema for schema in turn_contract.schemas()
|
||||
if (schema.get("function", {}).get("name") or schema.get("name"))
|
||||
not in disabled_tools
|
||||
]
|
||||
return _apply_tool_surface_to_schemas(contract_schemas, tool_surface)
|
||||
if route_state["is_api_model"]:
|
||||
if tool_surface == "full":
|
||||
# Full/regular models own semantic tool choice. Offer every
|
||||
@@ -29248,7 +29307,25 @@ async def stream_agent_loop(
|
||||
logger.info(
|
||||
"[agent] removed trailing private answer promise after successful tool result"
|
||||
)
|
||||
if tool_blocks and (_is_tool_preamble(cleaned_round) or _looks_like_agent_reasoning_preamble(cleaned_round)):
|
||||
if (
|
||||
tool_blocks
|
||||
and _contextual_weather_status_followup
|
||||
and any(block.tool_type in WEB_TOOL_NAMES for block in tool_blocks)
|
||||
):
|
||||
for _idx, _earlier_text in enumerate(round_texts):
|
||||
if str(_earlier_text or "").rstrip().endswith("?"):
|
||||
full_response = _drop_rejected_round_response(full_response, _earlier_text)
|
||||
round_texts[_idx] = ""
|
||||
_dropped_tool_preamble_from_stream = True
|
||||
if tool_blocks and (
|
||||
_is_tool_preamble(cleaned_round)
|
||||
or _looks_like_agent_reasoning_preamble(cleaned_round)
|
||||
or (
|
||||
_contextual_weather_status_followup
|
||||
and cleaned_round.rstrip().endswith("?")
|
||||
and any(block.tool_type in WEB_TOOL_NAMES for block in tool_blocks)
|
||||
)
|
||||
):
|
||||
# The model's "I'll fetch..." sentence is useful as internal
|
||||
# progress but is not the answer. It has already streamed, so
|
||||
# remove it from the final/history response before the next tool
|
||||
@@ -33037,6 +33114,43 @@ async def stream_agent_loop(
|
||||
and isinstance(result, dict)
|
||||
and not result.get("error")
|
||||
):
|
||||
if _should_retry_empty_search_in_browser(
|
||||
result, disabled_tools, tool_policy,
|
||||
_empty_search_browser_fallback_done,
|
||||
):
|
||||
_empty_search_browser_fallback_done = True
|
||||
_browser_query = _web_search_query_from_block(block)
|
||||
_browser_url = "https://www.bing.com/search?" + urlencode({"q": _browser_query})
|
||||
_browser_block = ToolBlock(
|
||||
"private_browser",
|
||||
json.dumps({"action": "batch", "commands": [["open", _browser_url], ["snapshot"]]}),
|
||||
)
|
||||
yield f'data: {json.dumps({"type": "tool_start", "tool": "private_browser", "command": _browser_url, "round": round_num, "fallback": "empty_web_search"})}\n\n'
|
||||
try:
|
||||
_, _browser_result = await execute_tool_block(
|
||||
_browser_block,
|
||||
session_id=session_id,
|
||||
disabled_tools=disabled_tools,
|
||||
tool_policy=tool_policy,
|
||||
owner=owner,
|
||||
workspace=workspace,
|
||||
security_context=run_security,
|
||||
client_runtime_context=client_runtime_context,
|
||||
)
|
||||
except Exception as _browser_exc:
|
||||
_browser_result = {"error": str(_browser_exc), "exit_code": 1}
|
||||
_browser_output = str(_browser_result.get("output") or _browser_result.get("error") or "")
|
||||
yield f'data: {json.dumps({"type": "tool_output", "tool": "private_browser", "command": _browser_url, "output": _truncate(_browser_output), "exit_code": _browser_result.get("exit_code")})}\n\n'
|
||||
if (
|
||||
_browser_result.get("exit_code") == 0
|
||||
and _browser_output.strip()
|
||||
and not _private_browser_blocked_by_bot_check(_browser_result)
|
||||
):
|
||||
result["output"] = (
|
||||
"Browser search fallback (untrusted page content; verify relevant links):\n"
|
||||
+ _browser_output[:12000]
|
||||
)
|
||||
result["evidence_status"] = "browser_fallback"
|
||||
_web_search_queries.append(_web_search_query_from_block(block))
|
||||
_web_search_completed = True
|
||||
_last_web_search_output = str(
|
||||
|
||||
Reference in New Issue
Block a user