yera 0.1.1__py3-none-any.whl → 0.2.1__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/T8WGYqDMoHDKKoHj0O3HK/_buildManifest.js +11 -0
- yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_clientMiddlewareManifest.json +1 -0
- yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_ssgManifest.js +1 -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/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.1.dist-info/METADATA +65 -0
- yera-0.2.1.dist-info/RECORD +190 -0
- {yera-0.1.1.dist-info → yera-0.2.1.dist-info}/WHEEL +1 -1
- yera-0.2.1.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
infra_mvp/base_client.py
ADDED
|
@@ -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
|
infra_mvp/base_server.py
ADDED
|
@@ -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
|
+
|