yera 0.1.0__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.
Files changed (192) hide show
  1. infra_mvp/base_client.py +29 -0
  2. infra_mvp/base_server.py +68 -0
  3. infra_mvp/monitoring/__init__.py +15 -0
  4. infra_mvp/monitoring/metrics.py +185 -0
  5. infra_mvp/stream/README.md +56 -0
  6. infra_mvp/stream/__init__.py +14 -0
  7. infra_mvp/stream/__main__.py +101 -0
  8. infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
  9. infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
  10. infra_mvp/stream/agents/reference/blocks/action.json +170 -0
  11. infra_mvp/stream/agents/reference/blocks/button.json +66 -0
  12. infra_mvp/stream/agents/reference/blocks/date.json +65 -0
  13. infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
  14. infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
  15. infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
  16. infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
  17. infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
  18. infra_mvp/stream/agents/reference/blocks/table.json +56 -0
  19. infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
  20. infra_mvp/stream/app.py +49 -0
  21. infra_mvp/stream/container.py +112 -0
  22. infra_mvp/stream/schemas/__init__.py +16 -0
  23. infra_mvp/stream/schemas/agent.py +24 -0
  24. infra_mvp/stream/schemas/interaction.py +28 -0
  25. infra_mvp/stream/schemas/session.py +30 -0
  26. infra_mvp/stream/server.py +321 -0
  27. infra_mvp/stream/services/__init__.py +12 -0
  28. infra_mvp/stream/services/agent_service.py +40 -0
  29. infra_mvp/stream/services/event_converter.py +83 -0
  30. infra_mvp/stream/services/session_service.py +247 -0
  31. yera/__init__.py +50 -1
  32. yera/agents/__init__.py +2 -0
  33. yera/agents/context.py +41 -0
  34. yera/agents/dataclasses.py +69 -0
  35. yera/agents/decorator.py +207 -0
  36. yera/agents/discovery.py +124 -0
  37. yera/agents/typing/__init__.py +0 -0
  38. yera/agents/typing/coerce.py +408 -0
  39. yera/agents/typing/utils.py +19 -0
  40. yera/agents/typing/validate.py +206 -0
  41. yera/cli.py +377 -0
  42. yera/config/__init__.py +1 -0
  43. yera/config/config_utils.py +164 -0
  44. yera/config/function_config.py +55 -0
  45. yera/config/logging.py +18 -0
  46. yera/config/tool_config.py +8 -0
  47. yera/config2/__init__.py +8 -0
  48. yera/config2/dataclasses.py +534 -0
  49. yera/config2/keyring.py +270 -0
  50. yera/config2/paths.py +28 -0
  51. yera/config2/read.py +113 -0
  52. yera/config2/setup.py +109 -0
  53. yera/config2/setup_handlers/__init__.py +1 -0
  54. yera/config2/setup_handlers/anthropic.py +126 -0
  55. yera/config2/setup_handlers/azure.py +236 -0
  56. yera/config2/setup_handlers/base.py +125 -0
  57. yera/config2/setup_handlers/llama_cpp.py +205 -0
  58. yera/config2/setup_handlers/ollama.py +157 -0
  59. yera/config2/setup_handlers/openai.py +137 -0
  60. yera/config2/write.py +87 -0
  61. yera/dsl/__init__.py +0 -0
  62. yera/dsl/functions.py +94 -0
  63. yera/dsl/struct.py +20 -0
  64. yera/dsl/workspace.py +79 -0
  65. yera/events/__init__.py +57 -0
  66. yera/events/blocks/__init__.py +68 -0
  67. yera/events/blocks/action.py +57 -0
  68. yera/events/blocks/bar_chart.py +92 -0
  69. yera/events/blocks/base/__init__.py +20 -0
  70. yera/events/blocks/base/base.py +166 -0
  71. yera/events/blocks/base/chart.py +288 -0
  72. yera/events/blocks/base/layout.py +111 -0
  73. yera/events/blocks/buttons.py +37 -0
  74. yera/events/blocks/columns.py +26 -0
  75. yera/events/blocks/container.py +24 -0
  76. yera/events/blocks/date_picker.py +50 -0
  77. yera/events/blocks/exit.py +39 -0
  78. yera/events/blocks/form.py +24 -0
  79. yera/events/blocks/input_echo.py +22 -0
  80. yera/events/blocks/input_request.py +31 -0
  81. yera/events/blocks/line_chart.py +97 -0
  82. yera/events/blocks/markdown.py +67 -0
  83. yera/events/blocks/slider.py +54 -0
  84. yera/events/blocks/spinner.py +55 -0
  85. yera/events/blocks/system_prompt.py +22 -0
  86. yera/events/blocks/table.py +291 -0
  87. yera/events/models/__init__.py +39 -0
  88. yera/events/models/block_data.py +112 -0
  89. yera/events/models/in_event.py +7 -0
  90. yera/events/models/out_event.py +75 -0
  91. yera/events/runtime.py +187 -0
  92. yera/events/stream.py +91 -0
  93. yera/models/__init__.py +0 -0
  94. yera/models/data_classes.py +20 -0
  95. yera/models/llm_atlas_proxy.py +44 -0
  96. yera/models/llm_context.py +99 -0
  97. yera/models/llm_interfaces/__init__.py +0 -0
  98. yera/models/llm_interfaces/anthropic.py +153 -0
  99. yera/models/llm_interfaces/aws_bedrock.py +14 -0
  100. yera/models/llm_interfaces/azure_openai.py +143 -0
  101. yera/models/llm_interfaces/base.py +26 -0
  102. yera/models/llm_interfaces/interface_registry.py +74 -0
  103. yera/models/llm_interfaces/llama_cpp.py +136 -0
  104. yera/models/llm_interfaces/mock.py +29 -0
  105. yera/models/llm_interfaces/ollama_interface.py +118 -0
  106. yera/models/llm_interfaces/open_ai.py +150 -0
  107. yera/models/llm_workspace.py +19 -0
  108. yera/models/model_atlas.py +139 -0
  109. yera/models/model_definition.py +38 -0
  110. yera/models/model_factory.py +33 -0
  111. yera/opaque/__init__.py +9 -0
  112. yera/opaque/base.py +20 -0
  113. yera/opaque/decorator.py +8 -0
  114. yera/opaque/markdown.py +57 -0
  115. yera/opaque/opaque_function.py +25 -0
  116. yera/tools/__init__.py +29 -0
  117. yera/tools/atlas_tool.py +20 -0
  118. yera/tools/base.py +24 -0
  119. yera/tools/decorated_tool.py +18 -0
  120. yera/tools/decorator.py +35 -0
  121. yera/tools/tool_atlas.py +51 -0
  122. yera/tools/tool_utils.py +361 -0
  123. yera/ui/dist/404.html +1 -0
  124. yera/ui/dist/__next.__PAGE__.txt +10 -0
  125. yera/ui/dist/__next._full.txt +23 -0
  126. yera/ui/dist/__next._head.txt +6 -0
  127. yera/ui/dist/__next._index.txt +5 -0
  128. yera/ui/dist/__next._tree.txt +7 -0
  129. yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
  130. yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
  131. yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
  132. yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
  133. yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
  134. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  135. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  136. yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
  137. yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
  138. yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
  139. yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
  140. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
  141. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
  142. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
  143. yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
  144. yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
  145. yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
  146. yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
  147. yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
  148. yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
  149. yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  150. yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
  151. yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
  152. yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  153. yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  154. yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  155. yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
  156. yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
  157. yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
  158. yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
  159. yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  160. yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  161. yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
  162. yera/ui/dist/_not-found/__next._full.txt +14 -0
  163. yera/ui/dist/_not-found/__next._head.txt +6 -0
  164. yera/ui/dist/_not-found/__next._index.txt +5 -0
  165. yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
  166. yera/ui/dist/_not-found/__next._not-found.txt +4 -0
  167. yera/ui/dist/_not-found/__next._tree.txt +2 -0
  168. yera/ui/dist/_not-found.html +1 -0
  169. yera/ui/dist/_not-found.txt +14 -0
  170. yera/ui/dist/agent-icon.svg +3 -0
  171. yera/ui/dist/favicon.ico +0 -0
  172. yera/ui/dist/file.svg +1 -0
  173. yera/ui/dist/globe.svg +1 -0
  174. yera/ui/dist/index.html +1 -0
  175. yera/ui/dist/index.txt +23 -0
  176. yera/ui/dist/logo/full_logo.png +0 -0
  177. yera/ui/dist/logo/rune_logo.png +0 -0
  178. yera/ui/dist/logo/rune_logo_borderless.png +0 -0
  179. yera/ui/dist/logo/text_logo.png +0 -0
  180. yera/ui/dist/next.svg +1 -0
  181. yera/ui/dist/send.png +0 -0
  182. yera/ui/dist/send_single.png +0 -0
  183. yera/ui/dist/vercel.svg +1 -0
  184. yera/ui/dist/window.svg +1 -0
  185. yera/utils/__init__.py +1 -0
  186. yera/utils/path_utils.py +38 -0
  187. yera-0.2.0.dist-info/METADATA +65 -0
  188. yera-0.2.0.dist-info/RECORD +190 -0
  189. {yera-0.1.0.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
  190. yera-0.2.0.dist-info/entry_points.txt +2 -0
  191. yera-0.1.0.dist-info/METADATA +0 -11
  192. yera-0.1.0.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
+ )