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,29 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from .base_server import BaseServer
6
+
7
+
8
+ class BaseClient(ABC):
9
+ def __init__(self, url_base: str):
10
+ self.url_base = url_base
11
+
12
+ def _make_url(self, *parts: str) -> str:
13
+ return "/".join([self.url_base.rstrip("/"), *list(parts)])
14
+
15
+ @classmethod
16
+ @abstractmethod
17
+ def from_server(cls, server: "BaseServer") -> "BaseClient":
18
+ """Create client from server instance.
19
+
20
+ Must be implemented by each subclass with appropriate server type.
21
+
22
+ Args:
23
+ server: BaseServer instance to connect to
24
+
25
+ Returns:
26
+ BaseClient configured to connect to the server
27
+
28
+ """
29
+ raise NotImplementedError
@@ -0,0 +1,68 @@
1
+ from abc import ABC, abstractmethod
2
+ from threading import Thread
3
+ from time import sleep
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from uvicorn import Config, Server
8
+
9
+ from yera.config.logging import LOG_CONFIG
10
+
11
+
12
+ class BaseServer(Server, ABC):
13
+ def __init__(self, host: str | None = "127.0.0.1", port: int | None = 8989):
14
+ self.host = host
15
+ self.port = port
16
+ self.thread = None
17
+ app = self.build_api()
18
+ app = self._add_middleware(app)
19
+
20
+ # Expose the fully configured FastAPI app (including middleware such as CORS)
21
+ # so that external runners (e.g. uvicorn infra_mvp.stream.app:app) can
22
+ # reuse the same application instance.
23
+ self.app: FastAPI = app
24
+
25
+ super().__init__(Config(self.app, self.host, self.port, log_config=LOG_CONFIG))
26
+
27
+ @property
28
+ def url_base(self):
29
+ return f"http://{self.host}:{self.port}"
30
+
31
+ @abstractmethod
32
+ def build_api(self) -> FastAPI:
33
+ pass
34
+
35
+ def _add_middleware(self, app: FastAPI) -> FastAPI:
36
+ # Add CORS middleware for local development - enables Postman testing and future web frontends
37
+ app.add_middleware(
38
+ CORSMiddleware,
39
+ allow_origins=["*"], # Allow all origins for local development
40
+ allow_credentials=True,
41
+ allow_methods=["*"],
42
+ allow_headers=["*"],
43
+ )
44
+ return app
45
+
46
+ def start(self):
47
+ self.thread = Thread(target=self.run)
48
+ self.thread.start()
49
+ while not self.started:
50
+ if not self.thread.is_alive():
51
+ raise RuntimeError("Server failed to start")
52
+ sleep(0.1)
53
+
54
+ # If port was 0 (random), update it with the actual bound port
55
+ if self.port == 0:
56
+ # Update port with the actual bound port from the first socket
57
+ self.port = self.servers[0].sockets[0].getsockname()[1]
58
+
59
+ def stop(self):
60
+ self.should_exit = True
61
+ self.thread.join()
62
+
63
+ def __enter__(self) -> "BaseServer":
64
+ self.start()
65
+ return self
66
+
67
+ def __exit__(self, exc_type, exc_val, exc_tb):
68
+ self.stop()
@@ -0,0 +1,15 @@
1
+ """Monitoring and metrics utilities for Yera services."""
2
+
3
+ from .metrics import (
4
+ ServiceMetrics,
5
+ get_metrics_endpoint,
6
+ track_operation,
7
+ track_request,
8
+ )
9
+
10
+ __all__ = [
11
+ "ServiceMetrics",
12
+ "get_metrics_endpoint",
13
+ "track_operation",
14
+ "track_request",
15
+ ]
@@ -0,0 +1,185 @@
1
+ """Prometheus metrics integration for Yera services."""
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ from contextlib import contextmanager
6
+ from functools import wraps
7
+ from typing import Any
8
+
9
+ from fastapi import Response
10
+ from prometheus_client import (
11
+ CONTENT_TYPE_LATEST,
12
+ CollectorRegistry,
13
+ Counter,
14
+ Gauge,
15
+ Histogram,
16
+ generate_latest,
17
+ )
18
+
19
+
20
+ class ServiceMetrics:
21
+ """Prometheus metrics for a Yera service."""
22
+
23
+ def __init__(self, service_name: str, registry: CollectorRegistry | None = None):
24
+ """Initialize metrics for a service.
25
+
26
+ Args:
27
+ service_name: Name of the service (e.g., 'api', 'iam', 'stream')
28
+ registry: Prometheus registry to use. If None, creates a new isolated registry.
29
+
30
+ """
31
+ self.service_name = service_name
32
+ self.registry = registry or CollectorRegistry()
33
+ prefix = f"{service_name}_"
34
+
35
+ # Request metrics
36
+ self.requests_total = Counter(
37
+ f"{prefix}requests_total",
38
+ f"Total number of {service_name} API requests",
39
+ ["endpoint", "method", "status"],
40
+ registry=self.registry,
41
+ )
42
+
43
+ self.request_duration_seconds = Histogram(
44
+ f"{prefix}request_duration_seconds",
45
+ f"Duration of {service_name} API requests",
46
+ ["endpoint", "method"],
47
+ buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
48
+ registry=self.registry,
49
+ )
50
+
51
+ # Error metrics
52
+ self.errors_total = Counter(
53
+ f"{prefix}errors_total",
54
+ f"Total number of {service_name} errors",
55
+ ["operation", "error_type"],
56
+ registry=self.registry,
57
+ )
58
+
59
+ # Resource gauges (optional, can be extended)
60
+ self._gauges: dict[str, Gauge] = {}
61
+
62
+ def create_gauge(
63
+ self, name: str, description: str, labels: list[str] | None = None
64
+ ) -> Gauge:
65
+ """Create a custom gauge metric."""
66
+ full_name = f"{self.service_name}_{name}"
67
+ if full_name in self._gauges:
68
+ return self._gauges[full_name]
69
+
70
+ gauge = Gauge(full_name, description, labels or [], registry=self.registry)
71
+ self._gauges[full_name] = gauge
72
+ return gauge
73
+
74
+ def create_counter(
75
+ self, name: str, description: str, labels: list[str] | None = None
76
+ ) -> Counter:
77
+ """Create a custom counter metric."""
78
+ full_name = f"{self.service_name}_{name}"
79
+ return Counter(full_name, description, labels or [], registry=self.registry)
80
+
81
+ def create_histogram(
82
+ self,
83
+ name: str,
84
+ description: str,
85
+ labels: list[str] | None = None,
86
+ buckets: list[float] | None = None,
87
+ ) -> Histogram:
88
+ """Create a custom histogram metric."""
89
+ full_name = f"{self.service_name}_{name}"
90
+ return Histogram(
91
+ full_name,
92
+ description,
93
+ labels or [],
94
+ buckets=buckets,
95
+ registry=self.registry,
96
+ )
97
+
98
+ def track_request(
99
+ self,
100
+ endpoint: str,
101
+ method: str,
102
+ status_code: int,
103
+ duration: float | None = None,
104
+ ):
105
+ """Track an API request."""
106
+ self.requests_total.labels(
107
+ endpoint=endpoint, method=method, status=str(status_code)
108
+ ).inc()
109
+ if duration is not None:
110
+ self.request_duration_seconds.labels(
111
+ endpoint=endpoint, method=method
112
+ ).observe(duration)
113
+
114
+ def track_error(self, operation: str, error_type: str):
115
+ """Track an error."""
116
+ self.errors_total.labels(operation=operation, error_type=error_type).inc()
117
+
118
+ @contextmanager
119
+ def track_operation_time(self, endpoint: str, method: str):
120
+ """Context manager to track operation duration."""
121
+ start_time = time.time()
122
+ try:
123
+ yield
124
+ finally:
125
+ duration = time.time() - start_time
126
+ self.request_duration_seconds.labels(
127
+ endpoint=endpoint, method=method
128
+ ).observe(duration)
129
+
130
+
131
+ # Decorator for tracking requests
132
+ def track_request(metrics: ServiceMetrics, endpoint: str, method: str = "POST"):
133
+ """Decorator to track request metrics."""
134
+
135
+ def decorator(func: Callable) -> Callable:
136
+ @wraps(func)
137
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
138
+ start_time = time.time()
139
+ status_code = 200
140
+ try:
141
+ result = await func(*args, **kwargs)
142
+ return result
143
+ except Exception as e:
144
+ if hasattr(e, "status_code"):
145
+ status_code = e.status_code
146
+ else:
147
+ status_code = 500
148
+ raise
149
+ finally:
150
+ duration = time.time() - start_time
151
+ metrics.track_request(endpoint, method, status_code, duration)
152
+
153
+ return wrapper
154
+
155
+ return decorator
156
+
157
+
158
+ # Context manager for tracking operations
159
+ @contextmanager
160
+ def track_operation(metrics: ServiceMetrics, operation: str):
161
+ """Context manager to track operation execution."""
162
+ try:
163
+ yield
164
+ except Exception as e:
165
+ error_type = type(e).__name__
166
+ metrics.track_error(operation, error_type)
167
+ raise
168
+
169
+
170
+ # Metrics endpoint generator
171
+ def get_metrics_endpoint(metrics: ServiceMetrics):
172
+ """Get a FastAPI endpoint function for /metrics.
173
+
174
+ Args:
175
+ metrics: ServiceMetrics instance with the registry to use
176
+
177
+ """
178
+
179
+ async def metrics_endpoint() -> Response:
180
+ """Prometheus metrics endpoint."""
181
+ return Response(
182
+ content=generate_latest(metrics.registry), media_type=CONTENT_TYPE_LATEST
183
+ )
184
+
185
+ return metrics_endpoint
@@ -0,0 +1,56 @@
1
+ # Yera Stream Test Server
2
+
3
+ A minimal FastAPI-based server that streams predefined conversations over Server-Sent Events (SSE) for the Yera UI.
4
+
5
+ ## Running the Server
6
+
7
+ ### Simple start (no auto-reload)
8
+
9
+ ```bash
10
+ uv run python -m infra_mvp.stream --host 127.0.0.1 --port 8991
11
+ ```
12
+
13
+ ### Development mode (with auto-reload)
14
+
15
+ ```bash
16
+ uv run uvicorn infra_mvp.stream.app:app --host 0.0.0.0 --port 8991 --reload
17
+ ```
18
+
19
+ The development command is recommended while working on the UI or stream conversations, as it automatically restarts when code changes.
20
+
21
+ ## Key Endpoints
22
+
23
+ - `POST /api/session` – create a new streaming session for a given `conversation_id`
24
+ - `GET /api/stream?session_id=...` – stream conversation messages via SSE
25
+ - `POST /api/interaction?session_id=...` – send user interactions (buttons, input prompts, etc.)
26
+ - `GET /health` – simple health check
27
+
28
+ ## Conversations
29
+
30
+ By default, the server loads conversation JSON files from:
31
+
32
+ ```text
33
+ src/infra_mvp/stream/conversations/
34
+ ```
35
+
36
+ The integration tests use separate, test-only conversations under:
37
+
38
+ ```text
39
+ tests/integration/infra_mvp/stream/conversations/
40
+ ```
41
+
42
+ ## Testing
43
+
44
+ ```bash
45
+ uv run pytest tests/integration/infra_mvp/stream -q
46
+ ```
47
+
48
+ This runs fast, focused integration tests against the stream server.
49
+
50
+ For lower-level checks of data loading and helpers, you can also run the unit tests:
51
+
52
+ ```bash
53
+ uv run pytest tests/unit/infra_mvp/stream -q
54
+ ```
55
+
56
+
@@ -0,0 +1,14 @@
1
+ """Stream server module for SSE-based message streaming.
2
+
3
+ This module provides a standalone server for UI development and testing.
4
+ The streaming functionality will eventually be integrated into the main ApiServer.
5
+ """
6
+
7
+ from .container import create_container, create_stream_server
8
+ from .server import StreamServer
9
+
10
+ __all__ = [
11
+ "StreamServer",
12
+ "create_container",
13
+ "create_stream_server",
14
+ ]
@@ -0,0 +1,101 @@
1
+ """Main entry point for running StreamServer.
2
+
3
+ Usage:
4
+ python -m infra_mvp.stream --agent-path demos/agents/basic_chatbot.py
5
+ python -m infra_mvp.stream --agent-path demos/agents/basic_chatbot.py --port 8991
6
+ python -m infra_mvp.stream --agent-path demos/agents/basic_chatbot.py --host 0.0.0.0 --port 8991
7
+ """
8
+
9
+ import argparse
10
+ import os
11
+ import signal
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from infra_mvp.stream.container import create_stream_server
16
+ from yera.agents.discovery import load_agent
17
+
18
+
19
+ def main():
20
+ """Run StreamServer with command-line arguments."""
21
+ parser = argparse.ArgumentParser(
22
+ description="Start StreamServer for SSE streaming to Yera UI"
23
+ )
24
+ default_host = os.getenv("HOST", "0.0.0.0")
25
+ default_port = int(os.getenv("PORT", "8991"))
26
+
27
+ parser.add_argument(
28
+ "--agent-path",
29
+ type=str,
30
+ required=True,
31
+ help="Path to Python file containing an agent",
32
+ )
33
+ parser.add_argument(
34
+ "--agent",
35
+ type=str,
36
+ default=None,
37
+ help="Name of the agent to use when multiple agents exist in the file",
38
+ )
39
+ parser.add_argument(
40
+ "--host",
41
+ type=str,
42
+ default=default_host,
43
+ help=f"Server host address (default: {default_host})",
44
+ )
45
+ parser.add_argument(
46
+ "--port",
47
+ type=int,
48
+ default=default_port,
49
+ help=f"Server port number (default: {default_port})",
50
+ )
51
+ parser.add_argument(
52
+ "--debug",
53
+ action="store_true",
54
+ help="Enable debug logging of events to file (logs to yera-debug-<timestamp>.jsonl)",
55
+ )
56
+
57
+ args = parser.parse_args()
58
+
59
+ # Load Python agent
60
+ try:
61
+ agent_wrapper = load_agent(Path(args.agent_path), agent_name=args.agent)
62
+ except (FileNotFoundError, ValueError) as e:
63
+ print(f"Error loading agent: {e}", file=sys.stderr)
64
+ sys.exit(1)
65
+
66
+ # Create stream server with Python agent
67
+ server = create_stream_server(
68
+ agent=agent_wrapper,
69
+ host=args.host,
70
+ port=args.port,
71
+ debug=args.debug,
72
+ )
73
+
74
+ # Handle graceful shutdown
75
+ def signal_handler(sig, frame): # noqa: ARG001
76
+ print("\nShutting down server...")
77
+ server.stop()
78
+ sys.exit(0)
79
+
80
+ signal.signal(signal.SIGINT, signal_handler)
81
+ signal.signal(signal.SIGTERM, signal_handler)
82
+
83
+ # Start server
84
+ print(f"Starting StreamServer on {server.url_base}")
85
+ print("Press Ctrl+C to stop the server")
86
+ print(f"Health check: {server.url_base}/health")
87
+ print(f"API docs: {server.url_base}/docs")
88
+
89
+ try:
90
+ with server:
91
+ # Keep server running
92
+ import time
93
+
94
+ while True:
95
+ time.sleep(1)
96
+ except KeyboardInterrupt:
97
+ signal_handler(None, None)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
@@ -0,0 +1,170 @@
1
+ # Portfolio Assistant Demo - Chart Additions Plan
2
+
3
+ ## Overview
4
+
5
+ This document outlines the planned additions of charts to the Portfolio Assistant demo to make it more visually compelling and useful. These additions will be implemented after the necessary DTOs and chart type support are added to the codebase.
6
+
7
+ ## Planned Chart Additions
8
+
9
+ ### 1. Line Chart - Portfolio Performance Over Time
10
+
11
+ **Location:** After portfolio strategy description, before "Current Portfolio Exposures" section
12
+
13
+ **Purpose:**
14
+ - Provide historical context for the portfolio
15
+ - Show performance trends over time
16
+ - Help users understand why rebalancing might be needed
17
+
18
+ **Data to Display:**
19
+ - X-axis: Time periods (e.g., monthly or quarterly over 1-3 years)
20
+ - Y-axis: Portfolio value or cumulative return (%)
21
+ - Series: Portfolio performance, optionally with benchmark comparison
22
+
23
+ **Example Flow:**
24
+ ```
25
+ Portfolio Strategy Description
26
+
27
+ [Line Chart: Portfolio Performance Over Time] ← NEW
28
+
29
+ Current Portfolio Exposures (table)
30
+ ```
31
+
32
+ **Implementation Requirements:**
33
+ - Add `line_chart` block type support
34
+ - Create `LineChartData` DTO with:
35
+ - `x_categories`: list of time periods
36
+ - `y_values`: list of series (each series is a list of values)
37
+ - `x_label`: string (e.g., "Time")
38
+ - `y_label`: string (e.g., "Portfolio Value")
39
+ - Optional: `series_labels` for multiple series
40
+
41
+ ---
42
+
43
+ ### 2. Slider-Adjustable Side-by-Side Bar Chart - Risk/Allocation Relationship
44
+
45
+ **Location:** During risk adjustment flow, after the risk slider is displayed
46
+
47
+ **Purpose:**
48
+ - Visualize how portfolio allocation changes with different risk levels
49
+ - Show current allocation vs. target allocation at selected risk level
50
+ - Provide real-time feedback as user adjusts risk slider
51
+
52
+ **Data to Display:**
53
+ - X-axis: Asset classes/sectors (Bonds, Equities, Cash, etc.)
54
+ - Two series:
55
+ - "Current Allocation" (static)
56
+ - "Target at Risk X" (updates based on slider value)
57
+ - Y-axis: Allocation percentage (0-100%)
58
+
59
+ **Example Flow:**
60
+ ```
61
+ Risk Adjustment Prompt
62
+
63
+ [Risk Slider: 1-10] ← Existing
64
+
65
+ [Side-by-Side Bar Chart: Current vs Target Allocation] ← NEW (updates with slider)
66
+
67
+ "Updating risk level..." action
68
+ ```
69
+
70
+ **Implementation Requirements:**
71
+ - Extend `BarChartData` DTO to support:
72
+ - Slider binding/relationship
73
+ - Dynamic updates based on external state (slider value)
74
+ - Mechanism to link chart data to slider value
75
+
76
+ **Design Considerations:**
77
+ - How should the chart receive slider updates? (event-driven, reactive, etc.)
78
+ - Should the chart be a single block that updates, or multiple blocks?
79
+ - How do we structure the data to support multiple risk levels?
80
+ - Option A: Pre-compute all risk levels, chart selects based on slider
81
+ - Option B: Chart receives slider value and computes target allocation
82
+ - Option C: Chart is updated via separate events as slider changes
83
+
84
+ **Proposed Data Structure:**
85
+ ```json
86
+ {
87
+ "block_type": "bar_chart",
88
+ "data": {
89
+ "x_categories": ["Bonds", "Equities", "Cash"],
90
+ "y_values": [
91
+ [30, 65, 5], // Current allocation (static)
92
+ [30, 65, 5] // Target allocation (updates with slider)
93
+ ],
94
+ "colour_categories": ["Current", "Target at Risk 7"],
95
+ "x_label": "Asset Class",
96
+ "y_label": "Allocation %",
97
+ "slider_binding": {
98
+ "slider_block_id": "risk-level",
99
+ "value_mapping": {
100
+ "1": [[60, 30, 10]],
101
+ "2": [[55, 35, 10]],
102
+ // ... mappings for each risk level
103
+ "10": [[10, 85, 5]]
104
+ }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Implementation Order
113
+
114
+ ### Phase 1: Line Chart Support
115
+ 1. Add `line_chart` to block type Literal in `Event` model
116
+ 2. Create `LineChartData` DTO in `block_data.py`
117
+ 3. Implement `line_chart` block factory in `blocks/line_chart.py`
118
+ 4. Add tests in `test_blocks.py`
119
+ 5. Add line chart to demo JSON
120
+
121
+ ### Phase 2: Slider-Adjustable Bar Chart Support
122
+ 1. Design DTO structure for slider-binding (see considerations above)
123
+ 2. Extend `BarChartData` or create new variant
124
+ 3. Implement update mechanism (event-driven vs reactive)
125
+ 4. Add tests for slider-bound charts
126
+ 5. Add slider-adjustable bar chart to demo JSON
127
+
128
+ ### Phase 3: Demo Integration
129
+ 1. Add line chart after portfolio strategy descriptions (all three portfolio flows)
130
+ 2. Add slider-adjustable bar chart in risk adjustment flows (all three risk adjustment branches)
131
+ 3. Ensure chart data aligns with existing table data for consistency
132
+ 4. Test full demo flow
133
+
134
+ ---
135
+
136
+ ## Data Consistency Notes
137
+
138
+ - Line chart data should be consistent with portfolio performance mentioned in strategy descriptions
139
+ - Bar chart allocation data should align with:
140
+ - Current exposures tables
141
+ - Rebalancing strategy tables
142
+ - Risk level descriptions (e.g., Risk 7 = Moderate-High)
143
+
144
+ ---
145
+
146
+ ## Open Questions
147
+
148
+ 1. **Line Chart:**
149
+ - Should we show multiple series (portfolio vs benchmark)?
150
+ - What time period is most useful? (1Y, 3Y, 5Y?)
151
+ - Should we show absolute values or percentage returns?
152
+
153
+ 2. **Slider-Adjustable Bar Chart:**
154
+ - Should updates be immediate (as slider moves) or on slider release?
155
+ - How do we handle the initial state before slider interaction?
156
+ - Should we show a third series for "Change" (target - current)?
157
+
158
+ 3. **General:**
159
+ - Should charts replace tables or complement them?
160
+ - Do we need chart titles/captions in the DTOs?
161
+
162
+ ---
163
+
164
+ ## References
165
+
166
+ - Current demo: `portfolio_assistant_stream.json`
167
+ - Bar chart implementation: `src/yera/chat/blocks/bar_chart.py`
168
+ - Block data models: `src/yera/chat/models/block_data.py`
169
+ - Event model: `src/yera/chat/models/event.py`
170
+