From 9332e29e53427244cfce8201fcf2b6d1c6e0a202 Mon Sep 17 00:00:00 2001 From: roberto Date: Tue, 10 Mar 2026 09:11:24 +0100 Subject: [PATCH] bug fix sending component --- .gitignore | 1 + app/api/routes/device_ws.py | 21 ++++++++++++-- app/core/llm.py | 14 ++++++++-- app/core/orchestrator.py | 6 ++++ app/core/ws_context.py | 6 +++- app/db.py | 2 +- app/main.py | 8 ++++++ logging.conf | 56 +++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 9 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 logging.conf diff --git a/.gitignore b/.gitignore index 02654f8..b4418da 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ Thumbs.db # Claude Code .claude/ +logs/ diff --git a/app/api/routes/device_ws.py b/app/api/routes/device_ws.py index 7b9cf41..771b696 100644 --- a/app/api/routes/device_ws.py +++ b/app/api/routes/device_ws.py @@ -233,10 +233,19 @@ async def _handle_home_request( executor = await _make_ws_executor(websocket, user_id) set_client_executor(executor) response_chunks: list[str] = [] + agent_holder: list = [] try: - token_stream = orchestrate_v3_stream(user_id, message, context) + token_stream = orchestrate_v3_stream( + user_id, message, context, agent_holder=agent_holder + ) formatter = HomeFormatter(request_id=request_id, tool_results=[]) async for ws_frame in formatter.format(token_stream): + # Inject mutations from agent tool_results into stream_end + if ws_frame.type == "stream_end" and agent_holder: # type: ignore[union-attr] + ws_frame.mutations = [ # type: ignore[union-attr] + {"action": r["action"], "table": r["table"], "data": r["data"]} + for r in getattr(agent_holder[0], "tool_results", []) + ] await websocket.send_text(ws_frame.model_dump_json()) # Collect text chunks to build the full response for episode storage if ws_frame.type == "stream_text": # type: ignore[union-attr] @@ -278,10 +287,18 @@ async def _handle_floating_request( executor = await _make_ws_executor(websocket, user_id) set_client_executor(executor) response_chunks: list[str] = [] + agent_holder: list = [] try: - token_stream = orchestrate_v3_stream(user_id, message, context) + token_stream = orchestrate_v3_stream( + user_id, message, context, agent_holder=agent_holder + ) formatter = FloatingFormatter(request_id=request_id) async for ws_frame in formatter.format(token_stream): + if ws_frame.type == "stream_end" and agent_holder: # type: ignore[union-attr] + ws_frame.mutations = [ # type: ignore[union-attr] + {"action": r["action"], "table": r["table"], "data": r["data"]} + for r in getattr(agent_holder[0], "tool_results", []) + ] await websocket.send_text(ws_frame.model_dump_json()) if ws_frame.type == "stream_text": # type: ignore[union-attr] response_chunks.append(ws_frame.chunk) # type: ignore[union-attr] diff --git a/app/core/llm.py b/app/core/llm.py index 3d49157..3d985af 100644 --- a/app/core/llm.py +++ b/app/core/llm.py @@ -23,10 +23,15 @@ from openai import AsyncOpenAI import litellm from langchain_openai import ChatOpenAI +from langchain_litellm import ChatLiteLLM from litellm import get_supported_openai_params # noqa: F401 – validates install from app.config.settings import settings +# Some models (e.g. gpt-5, o-series) reject unsupported params like temperature. +# Drop them silently instead of raising UnsupportedParamsError. +litellm.drop_params = True + def _api_key_for_model(model: str) -> str | None: """Return the most appropriate API key for the given LiteLLM model string.""" @@ -48,7 +53,7 @@ def get_llm( *, model: str | None = None, temperature: float = 0, -) -> ChatOpenAI: +) -> ChatOpenAI | ChatLiteLLM: """Return a LangChain chat model backed by LiteLLM. LiteLLM exposes an OpenAI-compatible API, so we use ``ChatOpenAI`` pointed @@ -69,6 +74,11 @@ def get_llm( if settings.GITHUB_COPILOT_TOKEN_DIR: os.environ.setdefault("GITHUB_COPILOT_TOKEN_DIR", settings.GITHUB_COPILOT_TOKEN_DIR) + # Use ChatLiteLLM for provider-prefixed models (github_copilot/, anthropic/, etc.) + # so LiteLLM handles routing and auth. ChatOpenAI for plain OpenAI model names. + if "/" in model: + return ChatLiteLLM(model=model, temperature=temperature) + return ChatOpenAI( model=model, temperature=temperature, @@ -79,7 +89,7 @@ def get_llm( def get_router_llm( *, temperature: float = 0, -) -> ChatOpenAI: +) -> ChatOpenAI | ChatLiteLLM: """Return the lighter model used for intent classification / routing.""" return get_llm(model=settings.LLM_ROUTER_MODEL, temperature=temperature) diff --git a/app/core/orchestrator.py b/app/core/orchestrator.py index b9b96a4..7765704 100644 --- a/app/core/orchestrator.py +++ b/app/core/orchestrator.py @@ -162,17 +162,23 @@ async def orchestrate_v3_stream( message: str, context: dict[str, Any], reg: AgentRegistry | None = None, + agent_holder: list | None = None, ) -> AsyncGenerator[tuple[str, str], None]: """v3 streaming orchestration — yields (agent_name, token) pairs. The first yield always carries the agent_name with an empty token so that callers (e.g. FloatingFormatter) can detect the routing domain before any text tokens arrive. + + If *agent_holder* is provided (a list), the agent instance is appended so + callers can access ``agent.tool_results`` after the stream completes. """ if reg is None: reg = _default_registry agent_name = await classify_intent(message, context, reg) agent = reg.get(agent_name) + if agent_holder is not None: + agent_holder.append(agent) yield agent_name, "" # domain signal — no token yet async for token in agent.handle_stream(message, context): yield agent_name, token diff --git a/app/core/ws_context.py b/app/core/ws_context.py index d669c6e..14ac879 100644 --- a/app/core/ws_context.py +++ b/app/core/ws_context.py @@ -84,5 +84,9 @@ async def execute_on_client( result = await callback(payload) collector = _tool_result_collector.get(None) if collector is not None: - collector.append(result) + collector.append({ + "action": action, + "table": table, + "data": result, + }) return result diff --git a/app/db.py b/app/db.py index 38a8d27..07f88ad 100644 --- a/app/db.py +++ b/app/db.py @@ -24,7 +24,7 @@ from app.config.settings import settings engine = create_async_engine( settings.DATABASE_URL, pool_pre_ping=True, - echo=settings.ENV == "dev", + echo=False, ) async_session = async_sessionmaker(engine, expire_on_commit=False) diff --git a/app/main.py b/app/main.py index e3303ce..74c25ee 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,16 @@ from contextlib import asynccontextmanager +import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", +) +logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) +logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING) + from app.api.middleware.rate_limit import TierRateLimitMiddleware from app.api.middleware.sanitizer import SanitizerMiddleware from app.config.settings import settings diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..c5aeced --- /dev/null +++ b/logging.conf @@ -0,0 +1,56 @@ +[loggers] +keys=root,uvicorn,uvicorn.error,uvicorn.access,sqlalchemy,watchfiles + +[handlers] +keys=console,file + +[formatters] +keys=default + +[logger_root] +level=INFO +handlers=console,file + +[logger_uvicorn] +level=INFO +handlers= +qualname=uvicorn +propagate=1 + +[logger_uvicorn.error] +level=INFO +handlers= +qualname=uvicorn.error +propagate=1 + +[logger_uvicorn.access] +level=INFO +handlers= +qualname=uvicorn.access +propagate=1 + +[logger_sqlalchemy] +level=WARNING +handlers= +qualname=sqlalchemy +propagate=1 + +[logger_watchfiles] +level=WARNING +handlers= +qualname=watchfiles +propagate=1 + +[handler_console] +class=StreamHandler +formatter=default +args=(sys.stderr,) + +[handler_file] +class=logging.handlers.RotatingFileHandler +formatter=default +args=('logs/app.log', 'a', 10485760, 5, 'utf-8') + +[formatter_default] +format=%(asctime)s %(levelname)s %(name)s: %(message)s +datefmt=%Y-%m-%d %H:%M:%S diff --git a/requirements.txt b/requirements.txt index 7e2fbcd..ea10f59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ uvicorn[standard]>=0.34.0 gunicorn>=22.0.0 langchain>=0.3.0 langchain-openai>=0.3.0 +langchain-litellm>=0.1.0 litellm>=1.50.0 pydantic>=2.10.0 pydantic-settings>=2.7.0