Skip to content

Commit 836fc3a

Browse files
Merge pull request #53 from basedosdados/staging
chore: sync main with staging
2 parents 90c2bd7 + 51f2450 commit 836fc3a

17 files changed

Lines changed: 1338 additions & 942 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Claude
1+
# Agents
2+
.agents/
23
.claude/
34

45
# Python-generated files
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Add MODEL_CALL_LIMIT and INTERRUPTED message statuses.
2+
3+
Revision ID: 19e2c92563e2
4+
Revises: 1c6556bb74f2
5+
Create Date: 2026-05-14 16:02:45.217643
6+
"""
7+
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision: str = "19e2c92563e2"
14+
down_revision: Union[str, Sequence[str], None] = "1c6556bb74f2"
15+
branch_labels: Union[str, Sequence[str], None] = None
16+
depends_on: Union[str, Sequence[str], None] = None
17+
18+
19+
def upgrade() -> None:
20+
"""Add MODEL_CALL_LIMIT and INTERRUPTED to the messagestatus enum.
21+
22+
Note: `ALTER TYPE ... ADD VALUE` cannot run inside a transaction block
23+
on PostgreSQL pre-v12, so we use an autocommit block.
24+
"""
25+
with op.get_context().autocommit_block():
26+
op.execute(
27+
"ALTER TYPE messagestatus ADD VALUE IF NOT EXISTS 'MODEL_CALL_LIMIT'"
28+
)
29+
op.execute("ALTER TYPE messagestatus ADD VALUE IF NOT EXISTS 'INTERRUPTED'")
30+
31+
32+
def downgrade() -> None:
33+
"""Downgrade is intentionally unsupported.
34+
35+
Postgres cannot drop enum values, and rebuilding the type would require
36+
remapping existing MODEL_CALL_LIMIT and INTERRUPTED rows to another status,
37+
losing the diagnostic signal these statuses were added to capture.
38+
"""
39+
raise NotImplementedError(
40+
"Downgrade not supported: removing enum values would silently rewrite rows."
41+
)

app/api/dependencies/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .agent import Agent, get_agent
1+
from .agent import Agent, RunningRuns, get_agent, get_running_runs
22
from .auth import UserID, get_user_id
33
from .db import AsyncDB, get_database, get_session
44
from .feedback import FeedbackSender, get_feedback_sender
@@ -7,10 +7,12 @@
77
"Agent",
88
"AsyncDB",
99
"FeedbackSender",
10+
"RunningRuns",
1011
"UserID",
1112
"get_agent",
1213
"get_database",
1314
"get_feedback_sender",
15+
"get_running_runs",
1416
"get_session",
1517
"get_user_id",
1618
]

app/api/dependencies/agent.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from typing import Annotated
23

34
from fastapi import Depends, Request
@@ -8,4 +9,9 @@ def get_agent(request: Request) -> CompiledStateGraph:
89
return request.app.state.agent
910

1011

12+
def get_running_runs(request: Request) -> dict[str, asyncio.Task]:
13+
return request.app.state.running_runs
14+
15+
1116
Agent = Annotated[CompiledStateGraph, Depends(get_agent)]
17+
RunningRuns = Annotated[dict[str, asyncio.Task], Depends(get_running_runs)]

app/api/routers/chatbot.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
from fastapi import APIRouter, BackgroundTasks, HTTPException, status
55
from fastapi.responses import StreamingResponse
6+
from loguru import logger
67

7-
from app.api.dependencies import Agent, AsyncDB, FeedbackSender, UserID
8+
from app.api.dependencies import Agent, AsyncDB, FeedbackSender, RunningRuns, UserID
89
from app.api.schemas import ConfigDict, UserMessage
9-
from app.api.streaming import stream_response
10+
from app.api.streaming import run_agent, stream_events
11+
from app.api.streaming.schemas import StreamEvent
1012
from app.db.models import (
1113
FeedbackCreate,
1214
FeedbackPayload,
@@ -96,14 +98,19 @@ async def list_messages(
9698
return await database.get_messages(thread.id, order_by)
9799

98100

99-
@router.post("/threads/{thread_id}/messages")
101+
@router.post(
102+
"/threads/{thread_id}/messages",
103+
response_class=StreamingResponse,
104+
status_code=status.HTTP_201_CREATED,
105+
)
100106
async def send_message(
101107
thread_id: str,
102108
user_message: UserMessage,
103-
agent: Agent,
104109
database: AsyncDB,
110+
agent: Agent,
111+
running_runs: RunningRuns,
105112
user_id: UserID,
106-
) -> Message:
113+
) -> StreamingResponse:
107114
run_id = str(uuid.uuid4())
108115

109116
config = ConfigDict(
@@ -120,15 +127,35 @@ async def send_message(
120127

121128
message = await database.create_message(message_create)
122129

123-
return StreamingResponse(
124-
stream_response(
125-
database=database,
130+
queue: asyncio.Queue[StreamEvent] = asyncio.Queue()
131+
132+
task = asyncio.create_task(
133+
run_agent(
126134
agent=agent,
127-
user_message=message,
128135
config=config,
129136
thread_id=thread_id,
137+
user_message=message,
130138
model_uri=settings.MODEL_URI,
139+
queue=queue,
131140
),
141+
name=f"run_agent:{run_id}",
142+
)
143+
144+
running_runs[run_id] = task
145+
146+
def _cleanup(task: asyncio.Task): # pragma: no cover
147+
del running_runs[run_id]
148+
if task.cancelled():
149+
logger.warning(f"run_agent task {run_id} was cancelled mid-run")
150+
return
151+
e = task.exception()
152+
if e is not None:
153+
logger.opt(exception=e).error(f"run_agent task {run_id} crashed mid-run:")
154+
155+
task.add_done_callback(_cleanup)
156+
157+
return StreamingResponse(
158+
stream_events(queue),
132159
status_code=status.HTTP_201_CREATED,
133160
)
134161

app/api/streaming/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
from app.api.streaming.stream import stream_response
1+
from app.api.streaming.agent_runner import run_agent
2+
from app.api.streaming.stream import stream_events
23

3-
__all__ = ["stream_response"]
4+
__all__ = ["run_agent", "stream_events"]

0 commit comments

Comments
 (0)