diff --git a/backend/agent/router.py b/backend/agent/router.py index 01c2010..6c708dd 100644 --- a/backend/agent/router.py +++ b/backend/agent/router.py @@ -301,6 +301,9 @@ async def select_workflow( ) if fast: return fast + manifests = _filter_for_interface_settings(manifests, web_search_enabled=web_search_enabled) + if not manifests: + return fallback_result preferred = str(router_model or get_workflow_router_model_preference() or "").strip() model = preferred or str(chat_model or "").strip() @@ -316,26 +319,7 @@ async def select_workflow( if not model: return fallback_result - message_excerpt = str(message or "") - if len(message_excerpt) > 1800: - message_excerpt = message_excerpt[:1800] + "…" - history_chars = 300 if len(message_excerpt) > 1000 else 500 - compact_history = [ - {"role": item.get("role"), "content": str(item.get("content") or "")[:history_chars]} - for item in recent_messages[-4:] - ] - prompt = ( - "Select exactly one enabled workflow for the current request. Do not invent workflows. " - "Prefer the lowest-cost workflow that can complete the task. Return JSON only.\n\n" - f"Current request: {message_excerpt}\n" - f"Recent messages: {json.dumps(compact_history, ensure_ascii=False)}\n" - f"Attachment count: {len(attachments)}\n" - f"Selected knowledge database: {library_slug or 'none'}\n" - f"Web search mode: {'forced' if web_search_enabled else 'automatic'}\n" - "Web workflows are available in automatic mode even when not forced. If web search mode is forced, select a web-capable workflow unless a higher-priority write or attachment workflow is required. Knowledge workflows still require a selected database; vision workflows still require attachments.\n" - "When selecting any workflow with the web capability, inputs MUST include web_search_query and web_search_queries. These must be concise search-engine queries, resolved from recent conversation when the current request is a follow-up. Do not copy conversational filler; search for the factual topic.\n" - f"Enabled workflows: {json.dumps(manifests, ensure_ascii=False)}" - ) + prompt = _router_prompt(message=message, recent_messages=recent_messages, attachments=attachments, manifests=manifests) try: result = await chat_typed( model, diff --git a/backend/tests/test_workflow_router_builtins.py b/backend/tests/test_workflow_router_builtins.py index 73bb2ce..ddcf010 100644 --- a/backend/tests/test_workflow_router_builtins.py +++ b/backend/tests/test_workflow_router_builtins.py @@ -138,6 +138,43 @@ class RouterAndBuiltinTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(mocked.await_args.args[0], "chat") db.close() + async def test_router_prompt_formats_followup_context_and_filters_for_forced_web(self): + db = self.Session() + web = db.query(WorkflowDefinition).filter_by(slug="web-answer").one() + with patch( + "backend.agent.router.chat_typed", + new=AsyncMock(return_value=OllamaChatResult(content=json.dumps({ + "workflow_id": web.id, + "confidence": 0.95, + "reason": "current follow-up", + "inputs": {"web_search_query": "Iran political news today", "web_search_queries": ["Iran political news today"]}, + }))), + ) as mocked: + result = await select_workflow( + db, + message="and what's the news politically?", + recent_messages=[ + {"role": "user", "content": "what's the weather in Iran today"}, + {"role": "assistant", "content": "The weather in Tehran, Iran today is clear."}, + ], + attachments=[], + library_slug=None, + router_model=None, + chat_model="model", + web_search_enabled=True, + ) + + prompt = mocked.await_args.args[1][0]["content"] + self.assertEqual(result["inputs"]["web_search_query"], "Iran political news today") + self.assertIn("Recent conversation:\n1. User: what's the weather in Iran today", prompt) + self.assertIn("search for 'Iran political news today'", prompt) + self.assertIn("Allowed workflows. This list is already filtered", prompt) + self.assertIn("web-answer", prompt) + self.assertNotIn("input-output", prompt) + self.assertNotIn("Web search mode:", prompt) + self.assertNotIn("Selected knowledge database:", prompt) + db.close() + async def test_all_builtins_validate(self): registry = NativeToolProvider(); register_native_tools(registry) for item in BUILTIN_WORKFLOWS: