mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
Stabilize chat and cookbook workflows
This commit is contained in:
+126
-23
@@ -1076,6 +1076,16 @@ def _turn_targets_active_document(intent: Dict[str, object], last_user: str, act
|
||||
text,
|
||||
):
|
||||
return True
|
||||
if re.search(
|
||||
r"\b(?:add|insert|include|apply|put)\b.+\b(?:to it|to this|there|in it|in this|in the text|in the document)\b",
|
||||
text,
|
||||
):
|
||||
return True
|
||||
if re.search(
|
||||
r"\b(?:make it|make this|expand it|expand this|extend it|extend this|continue it|continue this)\b.*\b(?:longer|shorter|bigger|smaller|more detailed|more concise|expanded|extended)?\b",
|
||||
text,
|
||||
):
|
||||
return True
|
||||
return bool(re.search(
|
||||
r"\b("
|
||||
r"document|doc|draft|text|poem|story|essay|outline|letter|paragraph|"
|
||||
@@ -1149,13 +1159,41 @@ def _minimal_odysseus_doc_messages(messages: List[Dict], active_document, stream
|
||||
"markdown\n"
|
||||
"Document content\n"
|
||||
"```\n"
|
||||
"Do not use function calls or tool calls. Do not write anything before the fence. "
|
||||
"Do not use native function-call JSON or <tool_calls> markup. "
|
||||
"Use only the fenced document block above. Do not write anything before the fence. "
|
||||
"Use saved user memory facts when the user asks for something relating to them."
|
||||
)
|
||||
else:
|
||||
system = (
|
||||
"You are Odysseus. Use the provided function when it is needed. "
|
||||
"After a successful tool call, answer briefly."
|
||||
"You are Odysseus. Edit or suggest changes to the active document using exactly one fenced tool block when needed.\n"
|
||||
"If the user asks to add, remove, rewrite, transform, change, capitalize, shorten, expand, or otherwise apply a change, use edit_document or update_document, not suggest_document.\n"
|
||||
"Use suggest_document only when the user explicitly asks for suggestions, feedback, or proposed improvements without applying them.\n"
|
||||
"For targeted edits:\n"
|
||||
"```edit_document\n"
|
||||
"<<<FIND>>>\n"
|
||||
"exact text from the active document\n"
|
||||
"<<<REPLACE>>>\n"
|
||||
"replacement text\n"
|
||||
"<<<END>>>\n"
|
||||
"```\n"
|
||||
"For full rewrites only:\n"
|
||||
"```update_document\n"
|
||||
"entire new document content\n"
|
||||
"```\n"
|
||||
"For improvement suggestions:\n"
|
||||
"```suggest_document\n"
|
||||
"<<<FIND>>>\n"
|
||||
"text to improve\n"
|
||||
"<<<SUGGEST>>>\n"
|
||||
"suggested replacement\n"
|
||||
"<<<REASON>>>\n"
|
||||
"why this improves it\n"
|
||||
"<<<END>>>\n"
|
||||
"```\n"
|
||||
"Do not use native function-call JSON or <tool_calls> markup. "
|
||||
"FIND text must be copied exactly from the active document with no labels like content:, title:, or markdown. "
|
||||
"Use only the fenced tool blocks above. Do not write anything before the fenced block. "
|
||||
"After the tool succeeds, Odysseus will answer Done."
|
||||
)
|
||||
out = [{"role": "system", "content": system}]
|
||||
memory_message = _minimal_saved_memory_message(messages)
|
||||
@@ -1177,9 +1215,51 @@ def _minimal_odysseus_doc_messages(messages: List[Dict], active_document, stream
|
||||
return out
|
||||
|
||||
|
||||
def _normalize_stream_document_fences(text: str) -> str:
|
||||
"""Treat visible ```document blocks as create_document tool blocks."""
|
||||
return re.sub(r"```document(\s*\n)", r"```create_document\1", text or "")
|
||||
_DOC_MODEL_ARTIFACT_RE = re.compile(
|
||||
r"(?:\|end\|)+\|?assistan(?:t)?\|?"
|
||||
r"|\|assistan(?:t)?\|"
|
||||
r"|<\|im_start\|>\s*assistant"
|
||||
r"|<\|im_end\|>",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _strip_doc_model_artifacts(text: str) -> str:
|
||||
return _DOC_MODEL_ARTIFACT_RE.sub("", text or "")
|
||||
|
||||
|
||||
def _normalize_stream_document_fences(text: str, target_tool: str = "create_document") -> str:
|
||||
"""Treat visible ```document/documen blocks as document tool blocks.
|
||||
|
||||
The document LoRA occasionally emits a neutral/truncated `documen` fence.
|
||||
For new documents that maps to create_document. For active-document turns,
|
||||
the same shape is a full replacement of the open document, so map it to
|
||||
update_document and drop the title/language header lines.
|
||||
"""
|
||||
text = _strip_doc_model_artifacts(text or "")
|
||||
|
||||
def repl(match: re.Match) -> str:
|
||||
body = match.group(1) or ""
|
||||
if target_tool == "update_document":
|
||||
lines = body.splitlines()
|
||||
if lines and not lines[0].lstrip().startswith("#"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[0].strip().lower() in {
|
||||
"markdown", "md", "text", "txt", "html", "email",
|
||||
"python", "javascript", "typescript", "json", "yaml",
|
||||
}:
|
||||
lines = lines[1:]
|
||||
while lines and not lines[0].strip():
|
||||
lines = lines[1:]
|
||||
body = "\n".join(lines)
|
||||
return f"```{target_tool}\n{body}"
|
||||
|
||||
return re.sub(
|
||||
r"```documen(?:t)?\s*\n([\s\S]*?)(?=\n```|$)",
|
||||
repl,
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _recent_context_for_retrieval(messages: List[Dict], max_user: int = 3, max_chars: int = 600) -> str:
|
||||
@@ -2535,10 +2615,15 @@ async def stream_agent_loop(
|
||||
except Exception as _e:
|
||||
logger.debug(f"[tool-rag] skill-aware tool include skipped: {_e}")
|
||||
|
||||
_intent_domains = set(_intent.get("domains") or set())
|
||||
_ody_doc_finetune_mode = (
|
||||
(model or "").lower().startswith("odysseus-qwen3")
|
||||
and "documents" in (_intent.get("domains") or set())
|
||||
and "files" not in (_intent.get("domains") or set())
|
||||
and (
|
||||
"documents" in _intent_domains
|
||||
or _active_document_relevant
|
||||
or _prompt_active_document is not None
|
||||
)
|
||||
and "files" not in _intent_domains
|
||||
and not guide_only
|
||||
)
|
||||
_ody_doc_stream_create_mode = _ody_doc_finetune_mode and _prompt_active_document is None
|
||||
@@ -2808,6 +2893,7 @@ async def stream_agent_loop(
|
||||
_doc_opened = False # whether doc_stream_open was sent
|
||||
_doc_last_len = 0 # last content length sent
|
||||
_doc_stream_create_completed = False
|
||||
_ody_doc_tool_completed = False
|
||||
|
||||
# Set when the loop runs out of rounds while the agent was still actively
|
||||
# using tools — i.e. it was cut off, not finished. Drives a "Continue" event
|
||||
@@ -2863,14 +2949,7 @@ async def stream_agent_loop(
|
||||
]
|
||||
all_tool_schemas = base_schemas + mcp_schemas
|
||||
if _ody_doc_finetune_mode:
|
||||
if _ody_doc_stream_create_mode:
|
||||
all_tool_schemas = []
|
||||
else:
|
||||
_doc_schema_names = {"edit_document", "update_document", "suggest_document"}
|
||||
all_tool_schemas = [
|
||||
t for t in all_tool_schemas
|
||||
if t.get("function", {}).get("name") in _doc_schema_names
|
||||
]
|
||||
all_tool_schemas = []
|
||||
if disabled_tools:
|
||||
all_tool_schemas = [
|
||||
t for t in all_tool_schemas
|
||||
@@ -2916,7 +2995,7 @@ async def stream_agent_loop(
|
||||
max_tokens=max_tokens,
|
||||
prompt_type=prompt_type if round_num == 1 else None,
|
||||
tools=all_tool_schemas if all_tool_schemas else None,
|
||||
tool_choice_none=_ody_doc_stream_create_mode,
|
||||
tool_choice_none=_ody_doc_finetune_mode,
|
||||
timeout=agent_stream_timeout,
|
||||
session_id=session_id,
|
||||
):
|
||||
@@ -3040,9 +3119,12 @@ async def stream_agent_loop(
|
||||
if data.get("thinking"):
|
||||
round_reasoning += data["delta"]
|
||||
else:
|
||||
round_response += data["delta"]
|
||||
full_response += data["delta"]
|
||||
yield chunk # Stream all rounds
|
||||
_delta_text = _strip_doc_model_artifacts(data["delta"]) if _ody_doc_finetune_mode else data["delta"]
|
||||
round_response += _delta_text
|
||||
full_response += _delta_text
|
||||
data["delta"] = _delta_text
|
||||
if not _ody_doc_finetune_mode or data.get("thinking"):
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
# Detect text-fence doc streaming. Normal agent prompts
|
||||
# use ```create_document; the doc LoRA streaming path
|
||||
# uses neutral ```document to avoid triggering learned
|
||||
@@ -3053,7 +3135,7 @@ async def stream_agent_loop(
|
||||
and not (tool_policy and tool_policy.blocks("create_document"))
|
||||
):
|
||||
_fence_markers = (
|
||||
('```document\n', 'document')
|
||||
('```document\n', '```documen\n')
|
||||
if _ody_doc_stream_create_mode
|
||||
else ('```create_document\n',)
|
||||
)
|
||||
@@ -3122,12 +3204,20 @@ async def stream_agent_loop(
|
||||
_round_first_event_logged,
|
||||
_round_first_token_logged,
|
||||
)
|
||||
_normalized_doc_round = (
|
||||
_normalize_stream_document_fences(
|
||||
round_response,
|
||||
"create_document" if _ody_doc_stream_create_mode else "update_document",
|
||||
)
|
||||
if _ody_doc_finetune_mode
|
||||
else round_response
|
||||
)
|
||||
tool_blocks, used_native, converted_calls = _resolve_tool_blocks(
|
||||
_normalize_stream_document_fences(round_response) if _ody_doc_stream_create_mode else round_response,
|
||||
_normalized_doc_round,
|
||||
native_tool_calls,
|
||||
round_num,
|
||||
is_api_model=(_is_api_model and not guide_only),
|
||||
allow_fenced_for_api=_ody_doc_stream_create_mode,
|
||||
allow_fenced_for_api=_ody_doc_finetune_mode,
|
||||
)
|
||||
if _ody_doc_stream_create_mode and tool_blocks:
|
||||
create_idx = next(
|
||||
@@ -3782,6 +3872,12 @@ async def stream_agent_loop(
|
||||
and result.get("action") == "create"
|
||||
):
|
||||
_doc_stream_create_completed = True
|
||||
if (
|
||||
_ody_doc_finetune_mode
|
||||
and block.tool_type in ("create_document", "update_document", "edit_document", "suggest_document")
|
||||
and not result.get("error")
|
||||
):
|
||||
_ody_doc_tool_completed = True
|
||||
|
||||
# If budget was hit, stop the loop
|
||||
if budget_hit:
|
||||
@@ -3801,6 +3897,13 @@ async def stream_agent_loop(
|
||||
logger.info("[agent] odysseus doc stream-create completed after one create_document")
|
||||
break
|
||||
|
||||
if _ody_doc_tool_completed:
|
||||
if not full_response.strip() or full_response.strip().startswith("```"):
|
||||
full_response = "Done."
|
||||
yield 'data: ' + json.dumps({"delta": "Done."}) + '\n\n'
|
||||
logger.info("[agent] odysseus doc tool completed after one textual tool block")
|
||||
break
|
||||
|
||||
# Feed results back to LLM for next round
|
||||
# Pass the CONVERTED calls (aligned 1:1 with tool_result_texts), not the
|
||||
# raw native_tool_calls: a call that failed to convert is dropped from
|
||||
|
||||
@@ -110,6 +110,18 @@ _HARMONY_MARKERS = (
|
||||
)
|
||||
_HARMONY_MAX_MARKER_LEN = max(len(marker) for marker in _HARMONY_MARKERS)
|
||||
|
||||
_VISIBLE_CHAT_TEMPLATE_ARTIFACT_RE = re.compile(
|
||||
r"(?:\|end\|)+\|?assistan(?:t)?\|?"
|
||||
r"|\|assistan(?:t)?\|"
|
||||
r"|<\|im_start\|>\s*assistant"
|
||||
r"|<\|im_end\|>",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _strip_visible_chat_template_artifacts(text: str) -> str:
|
||||
return _VISIBLE_CHAT_TEMPLATE_ARTIFACT_RE.sub("", text or "")
|
||||
|
||||
|
||||
def _harmony_suffix_hold_len(text: str) -> int:
|
||||
"""Return how many trailing chars could be the start of a harmony marker."""
|
||||
@@ -2317,6 +2329,9 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
||||
if reasoning:
|
||||
yield _stream_delta_event(reasoning, thinking=True)
|
||||
if content:
|
||||
content = _strip_visible_chat_template_artifacts(content)
|
||||
if not content:
|
||||
continue
|
||||
content = re.sub(r"<mm:think(\s+[^>]*)?>", r"<think\1>", content, flags=re.IGNORECASE)
|
||||
content = re.sub(r"</mm:think>", "</think>", content, flags=re.IGNORECASE)
|
||||
stripped = content.lstrip()
|
||||
|
||||
Reference in New Issue
Block a user