Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/starter/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ OPENAI_BASE_URL=http://host.docker.internal:11434/v1

# Optional — read by the bank_name resolver.
# BANK_NAME=Northwind Bank

# CONTEXT_MAX_TOKENS=2000
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/agent_engine/core/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,3 @@ def _validate_node(
message="plugins/access.py is required for protected nodes",
)
)

77 changes: 70 additions & 7 deletions src/agent_manager/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
from agent_engine.runtime.streaming import RunStreamEvent
from agent_manager.api.deps import get_service
from agent_manager.api.schemas import (
ContextUsageResponse,
ConversationSummary,
CreateConversationRequest,
CreateConversationResponse,
FeedbackRequest,
FeedbackResponse,
MessageOut,
SendMessageRequest,
SendMessageResponse,
Expand All @@ -25,6 +29,7 @@
ConversationNotFound,
ConversationService,
ConversationTokenBudgetExceeded,
MessageNotFound,
)

router = APIRouter()
Expand All @@ -41,21 +46,57 @@ async def create_conversation(
return CreateConversationResponse(conversation_id=session_id, session_id=session_id)


@router.get("/conversations", response_model=list[ConversationSummary])
async def list_conversations(service: Service, user_id: str) -> list[ConversationSummary]:
sessions = await service.list_conversations(user_id)
return [
ConversationSummary(
conversation_id=s.session_id,
title=s.title,
last_message_at=s.last_message_at,
)
for s in sessions
]


@router.get("/conversations/{conversation_id}/messages", response_model=list[MessageOut])
async def list_messages(conversation_id: str, service: Service) -> list[MessageOut]:
try:
msgs = await service.history(conversation_id)
except ConversationNotFound as exc:
raise HTTPException(status_code=404, detail="conversation not found") from exc
return [MessageOut(role=m.role, content=m.content, created_at=m.created_at) for m in msgs]
return [
MessageOut(
message_id=m.message_id,
role=m.role,
content=m.content,
created_at=m.created_at,
feedback=m.feedback,
)
for m in msgs
]


@router.get("/conversations/{conversation_id}/usage", response_model=ContextUsageResponse)
async def get_usage(conversation_id: str, service: Service) -> ContextUsageResponse:
try:
usage = await service.usage(conversation_id)
except ConversationNotFound as exc:
raise HTTPException(status_code=404, detail="conversation not found") from exc
return ContextUsageResponse(
used_tokens=usage.used_tokens,
max_tokens=usage.max_tokens,
percent=usage.percent,
severity=usage.severity,
)


@router.post("/conversations/{conversation_id}/messages", response_model=SendMessageResponse)
async def send_message(
conversation_id: str, body: SendMessageRequest, service: Service
) -> SendMessageResponse:
try:
result = await service.send(conversation_id, body.message, user_id=body.user_id)
result, msg = await service.send(conversation_id, body.message, user_id=body.user_id)
except ConversationNotFound as exc:
raise HTTPException(status_code=404, detail="conversation not found") from exc
except ConversationTokenBudgetExceeded:
Expand All @@ -66,10 +107,30 @@ async def send_message(
answer=result.answer,
visited=list(result.visited),
used_tools=[ToolRecord(**dataclasses.asdict(t)) for t in result.used_tools],
message_id=msg.message_id if msg else None,
)


def _to_stream_event(event: RunStreamEvent) -> StreamEventOut:
@router.post(
"/conversations/{conversation_id}/messages/{message_id}/feedback",
response_model=FeedbackResponse,
)
async def record_feedback(
conversation_id: str,
message_id: str,
body: FeedbackRequest,
service: Service,
) -> FeedbackResponse:
try:
msg = await service.record_feedback(conversation_id, message_id, body.feedback)
except ConversationNotFound as exc:
raise HTTPException(status_code=404, detail="conversation not found") from exc
except MessageNotFound as exc:
raise HTTPException(status_code=404, detail="message not found") from exc
return FeedbackResponse(message_id=msg.message_id, feedback=msg.feedback)


def _to_stream_event(event: RunStreamEvent, message_id: str | None = None) -> StreamEventOut:
return StreamEventOut(
type=event.type,
content=event.content,
Expand All @@ -85,6 +146,7 @@ def _to_stream_event(event: RunStreamEvent) -> StreamEventOut:
if event.used_tools
else None
),
message_id=message_id,
)


Expand All @@ -106,10 +168,11 @@ async def stream_message(
async def event_source() -> AsyncIterator[str]:
try:
if first is not None:
payload = _to_stream_event(first).model_dump(exclude_none=True)
yield f"event: {first.type}\ndata: {json.dumps(payload)}\n\n"
async for event in stream:
payload = _to_stream_event(event).model_dump(exclude_none=True)
first_event, first_msg_id = first
payload = _to_stream_event(first_event, first_msg_id).model_dump(exclude_none=True)
yield f"event: {first_event.type}\ndata: {json.dumps(payload)}\n\n"
async for event, msg_id in stream:
payload = _to_stream_event(event, msg_id).model_dump(exclude_none=True)
yield f"event: {event.type}\ndata: {json.dumps(payload)}\n\n"
except Exception as exc:
yield f"event: error\ndata: {json.dumps({'type': 'error', 'error': str(exc)})}\n\n"
Expand Down
28 changes: 27 additions & 1 deletion src/agent_manager/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from pydantic import BaseModel

from agent_manager.domain import Role
from agent_manager.domain import ContextSeverity, Role


class CreateConversationRequest(BaseModel):
Expand All @@ -23,10 +23,27 @@ class CreateConversationResponse(BaseModel):
session_id: str


class ConversationSummary(BaseModel):
conversation_id: str
title: str | None = None
last_message_at: datetime | None = None


class MessageOut(BaseModel):
message_id: str | None = None
role: Role
content: str
created_at: datetime
feedback: str | None = None


class FeedbackRequest(BaseModel):
feedback: str | None = None


class FeedbackResponse(BaseModel):
message_id: str
feedback: str | None = None


class SendMessageRequest(BaseModel):
Expand All @@ -47,6 +64,14 @@ class SendMessageResponse(BaseModel):
answer: str
visited: list[str]
used_tools: list[ToolRecord]
message_id: str | None = None


class ContextUsageResponse(BaseModel):
used_tokens: int
max_tokens: int | None = None
percent: float = 0.0
severity: ContextSeverity = ContextSeverity.NORMAL


class StreamEventOut(BaseModel):
Expand All @@ -60,3 +85,4 @@ class StreamEventOut(BaseModel):
error: str | None = None
system_name: str | None = None
used_tools: list[ToolRecord] | None = None
message_id: str | None = None
11 changes: 5 additions & 6 deletions src/agent_manager/api/static/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@ <h1>Drop-in chat widget</h1>
<em>your</em> product, paste these two lines — pointing at your backend:</p>

<pre>&lt;script type="module" src="https://your-backend/widget.js"&gt;&lt;/script&gt;
&lt;agent-chat title="Home Assistant" color="#2563eb"&gt;&lt;/agent-chat&gt;</pre>
&lt;agent-chat title="Support" color="#18181b"&gt;&lt;/agent-chat&gt;</pre>

<p class="hint">↘ The launcher is in the bottom-right corner. Click it and try
“turn on the kitchen lights”, then “now turn it off”.</p>
<p class="hint">↘ The launcher is in the bottom-right corner. Click it and ask a question.</p>

<!-- The actual embed: defaults to this page's origin. -->
<agent-chat
title="Home Assistant"
color="#2563eb"
greeting="Hi! I can control your home — try ‘turn on the kitchen lights’."
title="Support"
color="#18181b"
greeting="How can I help you today?"
></agent-chat>
<script type="module" src="/widget.js"></script>
</body>
Expand Down
Loading
Loading