bug fix sending component

This commit is contained in:
2026-03-10 09:11:24 +01:00
parent 618076193a
commit 9332e29e53
9 changed files with 109 additions and 6 deletions

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ Thumbs.db
# Claude Code
.claude/
logs/

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

56
logging.conf Normal file
View File

@@ -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

View File

@@ -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