yera 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infra_mvp/base_client.py +29 -0
- infra_mvp/base_server.py +68 -0
- infra_mvp/monitoring/__init__.py +15 -0
- infra_mvp/monitoring/metrics.py +185 -0
- infra_mvp/stream/README.md +56 -0
- infra_mvp/stream/__init__.py +14 -0
- infra_mvp/stream/__main__.py +101 -0
- infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
- infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
- infra_mvp/stream/agents/reference/blocks/action.json +170 -0
- infra_mvp/stream/agents/reference/blocks/button.json +66 -0
- infra_mvp/stream/agents/reference/blocks/date.json +65 -0
- infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
- infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
- infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
- infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
- infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
- infra_mvp/stream/agents/reference/blocks/table.json +56 -0
- infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
- infra_mvp/stream/app.py +49 -0
- infra_mvp/stream/container.py +112 -0
- infra_mvp/stream/schemas/__init__.py +16 -0
- infra_mvp/stream/schemas/agent.py +24 -0
- infra_mvp/stream/schemas/interaction.py +28 -0
- infra_mvp/stream/schemas/session.py +30 -0
- infra_mvp/stream/server.py +321 -0
- infra_mvp/stream/services/__init__.py +12 -0
- infra_mvp/stream/services/agent_service.py +40 -0
- infra_mvp/stream/services/event_converter.py +83 -0
- infra_mvp/stream/services/session_service.py +247 -0
- yera/__init__.py +50 -1
- yera/agents/__init__.py +2 -0
- yera/agents/context.py +41 -0
- yera/agents/dataclasses.py +69 -0
- yera/agents/decorator.py +207 -0
- yera/agents/discovery.py +124 -0
- yera/agents/typing/__init__.py +0 -0
- yera/agents/typing/coerce.py +408 -0
- yera/agents/typing/utils.py +19 -0
- yera/agents/typing/validate.py +206 -0
- yera/cli.py +377 -0
- yera/config/__init__.py +1 -0
- yera/config/config_utils.py +164 -0
- yera/config/function_config.py +55 -0
- yera/config/logging.py +18 -0
- yera/config/tool_config.py +8 -0
- yera/config2/__init__.py +8 -0
- yera/config2/dataclasses.py +534 -0
- yera/config2/keyring.py +270 -0
- yera/config2/paths.py +28 -0
- yera/config2/read.py +113 -0
- yera/config2/setup.py +109 -0
- yera/config2/setup_handlers/__init__.py +1 -0
- yera/config2/setup_handlers/anthropic.py +126 -0
- yera/config2/setup_handlers/azure.py +236 -0
- yera/config2/setup_handlers/base.py +125 -0
- yera/config2/setup_handlers/llama_cpp.py +205 -0
- yera/config2/setup_handlers/ollama.py +157 -0
- yera/config2/setup_handlers/openai.py +137 -0
- yera/config2/write.py +87 -0
- yera/dsl/__init__.py +0 -0
- yera/dsl/functions.py +94 -0
- yera/dsl/struct.py +20 -0
- yera/dsl/workspace.py +79 -0
- yera/events/__init__.py +57 -0
- yera/events/blocks/__init__.py +68 -0
- yera/events/blocks/action.py +57 -0
- yera/events/blocks/bar_chart.py +92 -0
- yera/events/blocks/base/__init__.py +20 -0
- yera/events/blocks/base/base.py +166 -0
- yera/events/blocks/base/chart.py +288 -0
- yera/events/blocks/base/layout.py +111 -0
- yera/events/blocks/buttons.py +37 -0
- yera/events/blocks/columns.py +26 -0
- yera/events/blocks/container.py +24 -0
- yera/events/blocks/date_picker.py +50 -0
- yera/events/blocks/exit.py +39 -0
- yera/events/blocks/form.py +24 -0
- yera/events/blocks/input_echo.py +22 -0
- yera/events/blocks/input_request.py +31 -0
- yera/events/blocks/line_chart.py +97 -0
- yera/events/blocks/markdown.py +67 -0
- yera/events/blocks/slider.py +54 -0
- yera/events/blocks/spinner.py +55 -0
- yera/events/blocks/system_prompt.py +22 -0
- yera/events/blocks/table.py +291 -0
- yera/events/models/__init__.py +39 -0
- yera/events/models/block_data.py +112 -0
- yera/events/models/in_event.py +7 -0
- yera/events/models/out_event.py +75 -0
- yera/events/runtime.py +187 -0
- yera/events/stream.py +91 -0
- yera/models/__init__.py +0 -0
- yera/models/data_classes.py +20 -0
- yera/models/llm_atlas_proxy.py +44 -0
- yera/models/llm_context.py +99 -0
- yera/models/llm_interfaces/__init__.py +0 -0
- yera/models/llm_interfaces/anthropic.py +153 -0
- yera/models/llm_interfaces/aws_bedrock.py +14 -0
- yera/models/llm_interfaces/azure_openai.py +143 -0
- yera/models/llm_interfaces/base.py +26 -0
- yera/models/llm_interfaces/interface_registry.py +74 -0
- yera/models/llm_interfaces/llama_cpp.py +136 -0
- yera/models/llm_interfaces/mock.py +29 -0
- yera/models/llm_interfaces/ollama_interface.py +118 -0
- yera/models/llm_interfaces/open_ai.py +150 -0
- yera/models/llm_workspace.py +19 -0
- yera/models/model_atlas.py +139 -0
- yera/models/model_definition.py +38 -0
- yera/models/model_factory.py +33 -0
- yera/opaque/__init__.py +9 -0
- yera/opaque/base.py +20 -0
- yera/opaque/decorator.py +8 -0
- yera/opaque/markdown.py +57 -0
- yera/opaque/opaque_function.py +25 -0
- yera/tools/__init__.py +29 -0
- yera/tools/atlas_tool.py +20 -0
- yera/tools/base.py +24 -0
- yera/tools/decorated_tool.py +18 -0
- yera/tools/decorator.py +35 -0
- yera/tools/tool_atlas.py +51 -0
- yera/tools/tool_utils.py +361 -0
- yera/ui/dist/404.html +1 -0
- yera/ui/dist/__next.__PAGE__.txt +10 -0
- yera/ui/dist/__next._full.txt +23 -0
- yera/ui/dist/__next._head.txt +6 -0
- yera/ui/dist/__next._index.txt +5 -0
- yera/ui/dist/__next._tree.txt +7 -0
- yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
- yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
- yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
- yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
- yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
- yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
- yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
- yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
- yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
- yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
- yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
- yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
- yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
- yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
- yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
- yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
- yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
- yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
- yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
- yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
- yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
- yera/ui/dist/_not-found/__next._full.txt +14 -0
- yera/ui/dist/_not-found/__next._head.txt +6 -0
- yera/ui/dist/_not-found/__next._index.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.txt +4 -0
- yera/ui/dist/_not-found/__next._tree.txt +2 -0
- yera/ui/dist/_not-found.html +1 -0
- yera/ui/dist/_not-found.txt +14 -0
- yera/ui/dist/agent-icon.svg +3 -0
- yera/ui/dist/favicon.ico +0 -0
- yera/ui/dist/file.svg +1 -0
- yera/ui/dist/globe.svg +1 -0
- yera/ui/dist/index.html +1 -0
- yera/ui/dist/index.txt +23 -0
- yera/ui/dist/logo/full_logo.png +0 -0
- yera/ui/dist/logo/rune_logo.png +0 -0
- yera/ui/dist/logo/rune_logo_borderless.png +0 -0
- yera/ui/dist/logo/text_logo.png +0 -0
- yera/ui/dist/next.svg +1 -0
- yera/ui/dist/send.png +0 -0
- yera/ui/dist/send_single.png +0 -0
- yera/ui/dist/vercel.svg +1 -0
- yera/ui/dist/window.svg +1 -0
- yera/utils/__init__.py +1 -0
- yera/utils/path_utils.py +38 -0
- yera-0.2.0.dist-info/METADATA +65 -0
- yera-0.2.0.dist-info/RECORD +190 -0
- {yera-0.1.1.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
- yera-0.2.0.dist-info/entry_points.txt +2 -0
- yera-0.1.1.dist-info/METADATA +0 -11
- yera-0.1.1.dist-info/RECORD +0 -4
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Test server for SSE-based message streaming to Yera UI.
|
|
2
|
+
|
|
3
|
+
This is a standalone test server for UI development and testing.
|
|
4
|
+
The streaming functionality will eventually be integrated into the main ApiServer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import mimetypes
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from fastapi import Body, FastAPI, HTTPException
|
|
13
|
+
from fastapi import Path as FastAPIPath
|
|
14
|
+
from fastapi.responses import FileResponse, StreamingResponse
|
|
15
|
+
from fastapi.staticfiles import StaticFiles
|
|
16
|
+
|
|
17
|
+
from infra_mvp.base_server import BaseServer
|
|
18
|
+
from infra_mvp.monitoring import ServiceMetrics, get_metrics_endpoint
|
|
19
|
+
from infra_mvp.stream.schemas import (
|
|
20
|
+
AddInteractionResponse,
|
|
21
|
+
AgentMetadata,
|
|
22
|
+
CreateSessionResponse,
|
|
23
|
+
ListAgentsResponse,
|
|
24
|
+
SessionCreateRequest,
|
|
25
|
+
UserInteraction,
|
|
26
|
+
)
|
|
27
|
+
from infra_mvp.stream.services import AgentService, SessionService
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StreamServer(BaseServer):
|
|
31
|
+
"""Server that streams predefined conversation events via Server-Sent Events (SSE).
|
|
32
|
+
|
|
33
|
+
This server is intended for UI testing and development. The streaming functionality
|
|
34
|
+
will be integrated into the main ApiServer in the future.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
agent_service: AgentService,
|
|
40
|
+
session_service: SessionService,
|
|
41
|
+
host: str | None = "0.0.0.0",
|
|
42
|
+
port: int | None = 8991,
|
|
43
|
+
dev_ui: bool = False,
|
|
44
|
+
):
|
|
45
|
+
"""Initialise the stream server.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
agent_service: Agent management service.
|
|
49
|
+
session_service: Session management service.
|
|
50
|
+
host: Server host address. Defaults to "0.0.0.0".
|
|
51
|
+
port: Server port. Defaults to 8991 to avoid conflicts with other servers.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
# Store services and dependencies
|
|
55
|
+
self.agent_service = agent_service
|
|
56
|
+
self.session_service = session_service
|
|
57
|
+
self.dev_ui = dev_ui
|
|
58
|
+
|
|
59
|
+
# Initialize metrics
|
|
60
|
+
self.metrics = ServiceMetrics("stream")
|
|
61
|
+
|
|
62
|
+
super().__init__(host, port)
|
|
63
|
+
|
|
64
|
+
def stop(self):
|
|
65
|
+
"""Stop the server and clean up all sessions."""
|
|
66
|
+
# Clean up all sessions (terminates agent processes)
|
|
67
|
+
self.session_service.cleanup_all()
|
|
68
|
+
super().stop()
|
|
69
|
+
|
|
70
|
+
def build_api(self) -> FastAPI:
|
|
71
|
+
"""Build the FastAPI application with SSE streaming endpoints.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Configured FastAPI application
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
app = FastAPI(
|
|
78
|
+
title="Yera Stream Server",
|
|
79
|
+
description="SSE-based message streaming server for Yera UI development",
|
|
80
|
+
version="0.1.0",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@app.post("/api/session")
|
|
84
|
+
async def create_session(
|
|
85
|
+
request: SessionCreateRequest = Body(...),
|
|
86
|
+
) -> CreateSessionResponse:
|
|
87
|
+
"""Create a new session for streaming an agent.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
request: Session creation request with agent_id
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Response with session_id
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
HTTPException: 404 if agent not found
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
agent = self.agent_service.get_agent(request.agent_id)
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
103
|
+
|
|
104
|
+
session_id = self.session_service.create_session(agent)
|
|
105
|
+
return CreateSessionResponse(session_id=session_id)
|
|
106
|
+
|
|
107
|
+
@app.get("/api/session/{session_id}/stream")
|
|
108
|
+
async def stream_session(
|
|
109
|
+
session_id: str = FastAPIPath(
|
|
110
|
+
description="Session identifier for streaming"
|
|
111
|
+
),
|
|
112
|
+
) -> StreamingResponse:
|
|
113
|
+
"""Start streaming conversation events for a session via Server-Sent Events (SSE).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
session_id: Session identifier from the URL path.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
StreamingResponse with text/event-stream content type
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
HTTPException: 404 if session not found
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
self.session_service.get_session(session_id)
|
|
127
|
+
except KeyError:
|
|
128
|
+
raise HTTPException(
|
|
129
|
+
status_code=404, detail="Session not found"
|
|
130
|
+
) from None
|
|
131
|
+
|
|
132
|
+
return StreamingResponse(
|
|
133
|
+
self.session_service.generate_events(session_id),
|
|
134
|
+
media_type="text/event-stream",
|
|
135
|
+
headers={
|
|
136
|
+
"Cache-Control": "no-cache",
|
|
137
|
+
"Connection": "keep-alive",
|
|
138
|
+
"X-Accel-Buffering": "no", # Disable buffering in nginx
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@app.post("/api/session/{session_id}/interaction")
|
|
143
|
+
async def add_interaction(
|
|
144
|
+
session_id: str = FastAPIPath(description="Session identifier"),
|
|
145
|
+
interaction: UserInteraction = Body(description="User interaction data"),
|
|
146
|
+
) -> AddInteractionResponse:
|
|
147
|
+
"""Add a user interaction to a session.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: Session identifier from the URL path
|
|
151
|
+
interaction: User interaction data with blockId, value, and blockType
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Response indicating interaction was received
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
HTTPException: 404 if session not found
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
logging.info(
|
|
161
|
+
"Received user input: session_id=%s blockId=%s blockType=%s value=%s",
|
|
162
|
+
session_id,
|
|
163
|
+
interaction.blockId,
|
|
164
|
+
interaction.blockType,
|
|
165
|
+
interaction.value,
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
self.session_service.add_interaction(
|
|
169
|
+
session_id, interaction.model_dump()
|
|
170
|
+
)
|
|
171
|
+
except KeyError as e:
|
|
172
|
+
raise HTTPException(
|
|
173
|
+
status_code=404, detail=f"Session not found: {session_id}"
|
|
174
|
+
) from e
|
|
175
|
+
return AddInteractionResponse(status="received", session_id=session_id)
|
|
176
|
+
|
|
177
|
+
@app.get("/api/agents", response_model=ListAgentsResponse)
|
|
178
|
+
async def list_agents() -> ListAgentsResponse:
|
|
179
|
+
"""List all available agents with minimal metadata.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of agents with minimal metadata for listing/navigation
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
return ListAgentsResponse(
|
|
186
|
+
agents=[
|
|
187
|
+
AgentMetadata(
|
|
188
|
+
id=agent.metadata.identifier,
|
|
189
|
+
name=agent.metadata.name,
|
|
190
|
+
description=agent.metadata.description,
|
|
191
|
+
path=[],
|
|
192
|
+
)
|
|
193
|
+
for agent in self.agent_service.list_agents()
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@app.get("/health")
|
|
198
|
+
async def health_check() -> dict[str, str]:
|
|
199
|
+
"""Health check endpoint.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dictionary with health status
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
self.metrics.track_request("/health", "GET", 200)
|
|
206
|
+
return {"status": "healthy", "service": "yera-stream"}
|
|
207
|
+
|
|
208
|
+
# Add metrics endpoint
|
|
209
|
+
app.get("/metrics")(get_metrics_endpoint(self.metrics))
|
|
210
|
+
|
|
211
|
+
# Optionally configure static UI serving (must be after API routes)
|
|
212
|
+
self._configure_ui_routes(app)
|
|
213
|
+
|
|
214
|
+
return app
|
|
215
|
+
|
|
216
|
+
def _configure_ui_routes(self, app: FastAPI) -> None:
|
|
217
|
+
"""Configure static UI and SPA routing if enabled."""
|
|
218
|
+
if self.dev_ui:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
ui_path = _get_ui_static_path()
|
|
222
|
+
if not ui_path:
|
|
223
|
+
logging.error(
|
|
224
|
+
"Static UI requested (dev_ui=False) but no UI dist directory was found. "
|
|
225
|
+
"Ensure the yera UI bundle is installed or run in dev mode with the "
|
|
226
|
+
"Next.js dev server."
|
|
227
|
+
)
|
|
228
|
+
# Fail fast so callers know UI is unavailable in this mode
|
|
229
|
+
raise RuntimeError("Static UI not available")
|
|
230
|
+
logging.info("Serving static UI from %s", ui_path)
|
|
231
|
+
|
|
232
|
+
index_path = ui_path / "index.html"
|
|
233
|
+
if not index_path.is_file():
|
|
234
|
+
logging.error(
|
|
235
|
+
"Static UI requested (dev_ui=False) but index.html was not found at %s",
|
|
236
|
+
index_path,
|
|
237
|
+
)
|
|
238
|
+
raise RuntimeError("Static UI index.html not found")
|
|
239
|
+
|
|
240
|
+
static_next = ui_path / "_next"
|
|
241
|
+
if static_next.is_dir():
|
|
242
|
+
app.mount(
|
|
243
|
+
"/_next",
|
|
244
|
+
StaticFiles(directory=str(static_next)),
|
|
245
|
+
name="next-static",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
favicon_path = ui_path / "favicon.ico"
|
|
249
|
+
|
|
250
|
+
if favicon_path.is_file():
|
|
251
|
+
|
|
252
|
+
@app.get("/favicon.ico")
|
|
253
|
+
async def favicon() -> FileResponse:
|
|
254
|
+
return FileResponse(favicon_path)
|
|
255
|
+
|
|
256
|
+
@app.get("/")
|
|
257
|
+
async def serve_root() -> FileResponse:
|
|
258
|
+
"""Serve the SPA entrypoint for the root path."""
|
|
259
|
+
return FileResponse(index_path)
|
|
260
|
+
|
|
261
|
+
@app.get("/{full_path:path}")
|
|
262
|
+
async def serve_spa(full_path: str) -> FileResponse:
|
|
263
|
+
"""Serve static assets when they exist, otherwise serve index.html.
|
|
264
|
+
|
|
265
|
+
FastAPI registers more specific routes (API endpoints, /health, /metrics)
|
|
266
|
+
before this catch-all handler, so those are matched first. This handler
|
|
267
|
+
only receives routes that didn't match any other endpoint, and it acts
|
|
268
|
+
as a smart fallback:
|
|
269
|
+
|
|
270
|
+
- If the requested path corresponds to an actual file within the UI dist,
|
|
271
|
+
that file is served directly.
|
|
272
|
+
- Otherwise, index.html is served so the SPA router can handle the path
|
|
273
|
+
client-side.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
# Resolve path safely to avoid directory traversal outside ui_path
|
|
277
|
+
try:
|
|
278
|
+
candidate = (ui_path / full_path).resolve()
|
|
279
|
+
# Ensure candidate is within ui_path
|
|
280
|
+
if not candidate.is_relative_to(ui_path):
|
|
281
|
+
return FileResponse(index_path)
|
|
282
|
+
except (OSError, RuntimeError, ValueError):
|
|
283
|
+
# Any issues resolving the path - fall back to SPA entry
|
|
284
|
+
return FileResponse(index_path)
|
|
285
|
+
|
|
286
|
+
# If it's an actual file in the UI bundle, serve it with a sensible content type
|
|
287
|
+
if candidate.is_file():
|
|
288
|
+
media_type, _ = mimetypes.guess_type(str(candidate))
|
|
289
|
+
return FileResponse(candidate, media_type=media_type)
|
|
290
|
+
|
|
291
|
+
# Otherwise, let the SPA handle it
|
|
292
|
+
return FileResponse(index_path)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _get_ui_static_path() -> Path | None:
|
|
296
|
+
"""Locate the bundled UI static files, if available.
|
|
297
|
+
|
|
298
|
+
Order of precedence (explicit over implicit):
|
|
299
|
+
1. Environment variable YERA_UI_DIST
|
|
300
|
+
2. Packaged path relative to installed yera package: yera/ui/dist
|
|
301
|
+
"""
|
|
302
|
+
# First: explicit user configuration
|
|
303
|
+
env_path = os.getenv("YERA_UI_DIST")
|
|
304
|
+
if env_path:
|
|
305
|
+
candidate = Path(env_path).expanduser()
|
|
306
|
+
if candidate.is_dir() and (candidate / "index.html").is_file():
|
|
307
|
+
return candidate
|
|
308
|
+
|
|
309
|
+
# Second: packaged path (installed yera)
|
|
310
|
+
try:
|
|
311
|
+
import yera
|
|
312
|
+
|
|
313
|
+
package_root = Path(yera.__file__).parent
|
|
314
|
+
package_ui = package_root / "ui" / "dist"
|
|
315
|
+
if package_ui.is_dir() and (package_ui / "index.html").is_file():
|
|
316
|
+
return package_ui
|
|
317
|
+
except (ImportError, AttributeError, TypeError):
|
|
318
|
+
# yera not installed or __file__ not available (unlikely but safe)
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
return None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Stream services package."""
|
|
2
|
+
|
|
3
|
+
from .agent_service import AgentService
|
|
4
|
+
from .event_converter import output_event_to_json, user_interaction_to_input_event
|
|
5
|
+
from .session_service import SessionService
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentService",
|
|
9
|
+
"SessionService",
|
|
10
|
+
"output_event_to_json",
|
|
11
|
+
"user_interaction_to_input_event",
|
|
12
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from yera.agents.decorator import AgentFunctionWrapper
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentService:
|
|
5
|
+
"""Service for managing agents."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, agents: list[AgentFunctionWrapper]):
|
|
8
|
+
"""Initialise the agent service.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
agents: The list of agents to manage.
|
|
12
|
+
"""
|
|
13
|
+
self.agents: dict[str, AgentFunctionWrapper] = {
|
|
14
|
+
agent.metadata.identifier: agent for agent in agents
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def get_agent(self, agent_id: str) -> AgentFunctionWrapper:
|
|
18
|
+
"""Get an agent by ID.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
agent_id: The ID of the agent to get.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The agent function wrapper.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If the agent is not found.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
return self.agents[agent_id]
|
|
31
|
+
except KeyError:
|
|
32
|
+
raise ValueError(f"Agent with ID '{agent_id}' not found") from None
|
|
33
|
+
|
|
34
|
+
def list_agents(self) -> list[AgentFunctionWrapper]:
|
|
35
|
+
"""List all available agents.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of available agents.
|
|
39
|
+
"""
|
|
40
|
+
return list(self.agents.values())
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Event conversion utilities for converting between backend and frontend formats."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from yera.events.models.in_event import InputEvent
|
|
6
|
+
from yera.events.models.out_event import OutputEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def output_event_to_json(event: OutputEvent) -> dict:
|
|
10
|
+
"""Convert an OutputEvent to frontend JSON format.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
event: OutputEvent from the agent execution
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Dictionary matching the frontend AgentMessage interface:
|
|
17
|
+
{
|
|
18
|
+
"message_type": "informational" | "await_user" | "await_agent" | "layout",
|
|
19
|
+
"block_type": str,
|
|
20
|
+
"block_id": str,
|
|
21
|
+
"data": dict,
|
|
22
|
+
"timestamp": str (ISO format),
|
|
23
|
+
"chunk_id": int,
|
|
24
|
+
"agent": {
|
|
25
|
+
"name": str,
|
|
26
|
+
"instance_id": int
|
|
27
|
+
},
|
|
28
|
+
"parent_block_id": str | None
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
# Convert agent_instance to frontend format
|
|
32
|
+
agent = {
|
|
33
|
+
"name": event.agent_instance.agent_id,
|
|
34
|
+
"instance_id": event.agent_instance.instance_id,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Serialize data field (Pydantic models have model_dump())
|
|
38
|
+
# Use mode='json' to ensure date/datetime objects are serialized to strings
|
|
39
|
+
if hasattr(event.data, "model_dump"):
|
|
40
|
+
data = event.data.model_dump(mode="json")
|
|
41
|
+
else:
|
|
42
|
+
# Fallback for non-Pydantic data
|
|
43
|
+
data = event.data
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"message_type": event.message_type,
|
|
47
|
+
"block_type": event.block_type,
|
|
48
|
+
"block_id": event.block_id,
|
|
49
|
+
"data": data,
|
|
50
|
+
"timestamp": event.timestamp.isoformat(),
|
|
51
|
+
"chunk_id": event.chunk_id,
|
|
52
|
+
"agent": agent,
|
|
53
|
+
"parent_block_id": event.parent_block_id,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def user_interaction_to_input_event(interaction: dict) -> InputEvent:
|
|
58
|
+
"""Convert a UserInteraction dict to InputEvent format.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
interaction: UserInteraction dictionary with:
|
|
62
|
+
- blockId: str
|
|
63
|
+
- value: Any
|
|
64
|
+
- blockType: str
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
InputEvent for pushing to EventStream
|
|
68
|
+
"""
|
|
69
|
+
# Convert interaction value to string
|
|
70
|
+
# For buttons, value is typically the button text
|
|
71
|
+
# For text inputs, value is the text string
|
|
72
|
+
# For other types, convert to JSON string
|
|
73
|
+
if isinstance(interaction["value"], str):
|
|
74
|
+
value_str = interaction["value"]
|
|
75
|
+
else:
|
|
76
|
+
value_str = json.dumps(interaction["value"])
|
|
77
|
+
|
|
78
|
+
# Use blockId as identifier, blockType as type
|
|
79
|
+
return InputEvent(
|
|
80
|
+
identifier=interaction["blockId"],
|
|
81
|
+
type=interaction["blockType"],
|
|
82
|
+
data=value_str,
|
|
83
|
+
)
|