fix: portal routing + response speed + IME input

1. Portal unified routing: ws_chat now uses CostAwareRouter uniformly
   (handles Layer 0/1/2), replacing direct IntentRouter calls.
   Greeting/chat_mode requests skip IntentRouter LLM call entirely.

2. Response speed: greeting & simple chat now use direct LLM call
   (no ReAct loop), zero-cost Layer 0 detection.

3. IME input fix: use e.isComposing (native browser property)
   instead of compositionstart/end for Enter key detection.

4. Test: fix InMemoryMessageBus.request() parameter name
   timeout -> timeout_seconds.
This commit is contained in:
chiguyong 2026-06-11 21:30:25 +08:00
parent ae95b56465
commit 32c800d1e4
3 changed files with 71 additions and 35 deletions

View File

@ -171,6 +171,11 @@ class ReActEngine:
) -> ReActResult: ) -> ReActResult:
tools = tools or [] tools = tools or []
tool_schemas = self._build_tool_schemas(tools) if tools else None tool_schemas = self._build_tool_schemas(tools) if tools else None
if tool_schemas:
tool_names = [s["function"]["name"] for s in tool_schemas]
logger.info(f"ReActEngine executing with {len(tool_schemas)} tools: {tool_names}")
else:
logger.info("ReActEngine executing with NO tools")
# Telemetry: record agent request # Telemetry: record agent request
agent_request_counter().add(1, {"agent.name": agent_name, "agent.type": task_type or "react"}) agent_request_counter().add(1, {"agent.name": agent_name, "agent.type": task_type or "react"})
@ -478,6 +483,11 @@ class ReActEngine:
""" """
tools = tools or [] tools = tools or []
tool_schemas = self._build_tool_schemas(tools) if tools else None tool_schemas = self._build_tool_schemas(tools) if tools else None
if tool_schemas:
tool_names = [s["function"]["name"] for s in tool_schemas]
logger.info(f"ReActEngine executing with {len(tool_schemas)} tools: {tool_names}")
else:
logger.info("ReActEngine executing with NO tools")
# 启动轨迹记录 # 启动轨迹记录
if trace_recorder is not None: if trace_recorder is not None:

View File

@ -1,7 +1,3 @@
"""Portal API routes - unified chat interface with intent routing"""
from __future__ import annotations
import asyncio import asyncio
import hmac import hmac
import json import json
@ -516,51 +512,81 @@ async def portal_websocket(websocket: WebSocket):
if not message_text: if not message_text:
continue continue
# Save user message # Unified routing via CostAwareRouter (handles Layer 0/1/2)
_conversation_store.add_message(conv.id, "user", message_text)
# Resolve skill via IntentRouter
pool = websocket.app.state.agent_pool pool = websocket.app.state.agent_pool
skill_registry = websocket.app.state.skill_registry skill_registry = websocket.app.state.skill_registry
llm_gateway = websocket.app.state.llm_gateway
intent_router: IntentRouter = websocket.app.state.intent_router intent_router: IntentRouter = websocket.app.state.intent_router
cost_aware_router = websocket.app.state.cost_aware_router
all_skills = skill_registry.list_skills() all_skills = skill_registry.list_skills()
if not all_skills: if not all_skills:
await websocket.send_json( await websocket.send_json(
{ {"type": "error", "data": {"message": "No skills available"}}
"type": "error",
"data": {"message": "No skills available"},
}
) )
continue continue
try: # Get default tools for CostAwareRouter routing (only if default skill exists)
routing_result = await intent_router.route( default_tools = []
{"query": message_text, "sources": sources}, all_skills default_system_prompt = None
) default_agent = pool.get_agent("default")
await websocket.send_json( if default_agent is not None:
{ default_tools = default_agent.get_tools()
"type": "routing", default_system_prompt = default_agent.get_system_prompt()
"skill": routing_result.matched_skill, else:
"method": routing_result.method, # Fallback to first available skill's tools
"confidence": routing_result.confidence, for skill in all_skills:
} agent = pool.get_agent(skill.name)
) if agent is not None:
default_tools = agent.get_tools()
default_system_prompt = agent.get_system_prompt()
break
skill = skill_registry.get(routing_result.matched_skill) # Route via CostAwareRouter (Layer 0/1/2)
agent = pool.get_agent(routing_result.matched_skill) routing_result = await cost_aware_router.route(
if agent is None: content=message_text,
agent = await pool.create_agent_from_skill(routing_result.matched_skill) skill_registry=skill_registry,
except (ValueError, RuntimeError) as e: intent_router=intent_router,
await websocket.send_json( default_tools=default_tools,
{"type": "error", "data": {"message": str(e)}} default_system_prompt=default_system_prompt,
default_model="default",
default_agent_name="default",
session_id=conv.id,
transparency="SILENT",
)
await websocket.send_json({
"type": "routing",
"skill": routing_result.agent_name or "default",
"method": routing_result.match_method or "intent",
"confidence": routing_result.match_confidence,
})
# Execute based on routing method
if routing_result.match_method in ("greeting", "chat_mode"):
# Zero-cost path: direct LLM call, no ReAct loop
response = await llm_gateway.chat(
messages=[{"role": "user", "content": message_text}],
model="default",
agent_name="default",
task_type="chat",
) )
await websocket.send_json({
"type": "result",
"data": {"status": "completed", "content": response.content},
})
continue continue
# General path: agent execution
agent_name = routing_result.agent_name or "default"
agent = pool.get_agent(agent_name)
if agent is None:
agent = await pool.create_agent_from_skill(agent_name)
# Execute via ReAct stream # Execute via ReAct stream
react_config = agent.get_react_config() react_config = agent.get_react_config()
react_engine = ReActEngine( react_engine = ReActEngine(
llm_gateway=websocket.app.state.llm_gateway, llm_gateway=llm_gateway,
max_steps=react_config["max_steps"], max_steps=react_config["max_steps"],
) )

View File

@ -121,11 +121,11 @@ class TestInMemoryMessageBus:
payload={"q": "What is the answer?"}, payload={"q": "What is the answer?"},
) )
response = await bus.request(request, timeout=5.0) response = await bus.request(request, timeout_seconds=5.0)
assert response.payload["answer"] == 42 assert response.payload["answer"] == 42
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_request_timeout(self): async def test_request_timeout_seconds(self):
"""请求超时后返回 None。""" """请求超时后返回 None。"""
bus = InMemoryMessageBus() bus = InMemoryMessageBus()
@ -136,7 +136,7 @@ class TestInMemoryMessageBus:
topic="question", topic="question",
) )
result = await bus.request(request, timeout=0.1) result = await bus.request(request, timeout_seconds=0.1)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio