roam / backend /api /agent.py
AarnavNoble's picture
Deploy from GitHub main
4cfd686 verified
Raw History Blame Contribute Delete
2.69 kB
"""
Trip editing endpoint. The agent itself lives in backend/agent/; this module
only translates between HTTP and it, the same split as routes.py and pipeline.py.
An edit never overwrites a trip. It is stored as a new row whose parent_id
points at the trip it was made from, so the traveller can always go back.
"""
import asyncio
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from backend.agent.service import edit_trip
from backend.api.auth import optional_user
from backend.api.routes import _http_error
from backend.api.trips import load_trip, save_trip
from backend.core.ratelimit import TokenBucket, limiter
from backend.core.resilience import UpstreamError
from backend.db.models import User
from backend.db.session import get_db
router = APIRouter(tags=["agent"])
# 5 edits in a burst, then one every 15s, per client. An edit is several model
# calls, so it is limited separately from (and tighter than) itinerary creation.
edit_bucket = TokenBucket(capacity=5, rate=1 / 15)
class EditRequest(BaseModel):
message: str = Field(min_length=1, max_length=500)
@field_validator("message")
@classmethod
def _no_nul(cls, v: str) -> str:
if "\x00" in v:
raise ValueError("must not contain NUL bytes") # Postgres rejects them in text and JSON
return v
@router.post("/trips/{trip_id}/edit", dependencies=[Depends(limiter(edit_bucket))])
async def edit_trip_endpoint(trip_id: str, body: EditRequest, user: User | None = Depends(optional_user),
db: Session = Depends(get_db)):
"""Edit a trip from a plain-language request. Returns the new itinerary, or the old one if nothing changed."""
trip = load_trip(db, trip_id, user, write=True)
db.commit() # do not hold a database connection while the model thinks
try:
output = await asyncio.to_thread(edit_trip, trip.request, trip.route, trip.itinerary, body.message)
except Exception as e:
if not isinstance(e, (UpstreamError, ValueError)):
print(f"Edit error: {type(e).__name__}: {e}")
raise _http_error(e)
extra = {"changes": output.changes, "summary": output.summary, "steps": output.run.steps}
if not output.changed:
return trip.itinerary | {"trip_id": trip.id} | extra
new_trip = save_trip(db, user_id=trip.user_id, request=trip.request, route=output.route,
itinerary=output.result, degraded=output.degraded, parent_id=trip.id)
db.commit() # the new trip must be readable by the time the client has its id
return output.result | {"trip_id": new_trip.id} | extra