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.
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.1.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.1.dist-info/METADATA +0 -11
  192. yera-0.1.1.dist-info/RECORD +0 -4
@@ -0,0 +1,247 @@
1
+ """Session service for managing agent sessions."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import datetime
6
+ import json
7
+ import logging
8
+ import uuid
9
+ from collections.abc import AsyncIterator
10
+ from pathlib import Path
11
+
12
+ from infra_mvp.stream.schemas.session import SessionState
13
+ from infra_mvp.stream.services.event_converter import (
14
+ output_event_to_json,
15
+ user_interaction_to_input_event,
16
+ )
17
+ from yera.agents.decorator import AgentFunctionWrapper
18
+ from yera.events.runtime import PyRuntimeExecutor
19
+ from yera.events.stream import OutputEvent
20
+
21
+
22
+ class SessionService:
23
+ """Service for managing agent sessions and generating SSE events."""
24
+
25
+ def __init__(self, log_events: bool = False):
26
+ """Initialise the Python session service.
27
+
28
+ Args:
29
+ log_events: Enable logging of events to file
30
+ """
31
+ self._sessions: dict[str, SessionState] = {}
32
+ self._log_events = log_events
33
+ self._events_log_file: Path | None = None
34
+
35
+ if self._log_events:
36
+ # Create timestamped events log file
37
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
38
+ self._events_log_file = Path(f"yera-event-log-{timestamp}.jsonl")
39
+ logging.info(
40
+ f"Events logging enabled: events will be logged to {self._events_log_file}"
41
+ )
42
+
43
+ def generate_session_id(self) -> str:
44
+ """Generate a unique session ID.
45
+
46
+ Returns:
47
+ UUID string for the session
48
+ """
49
+ return str(uuid.uuid4())
50
+
51
+ def create_session(self, agent: AgentFunctionWrapper, args: tuple = ()) -> str:
52
+ """Create a new session for the agent.
53
+
54
+ Args:
55
+ agent: The agent wrapper to execute.
56
+ args: Positional arguments to pass to the agent's ``invoke`` method.
57
+
58
+ Returns:
59
+ Generated session_id
60
+ """
61
+ session_id = self.generate_session_id()
62
+ executor = PyRuntimeExecutor(agent, args)
63
+
64
+ session_state = SessionState(
65
+ agent=agent,
66
+ executor=executor,
67
+ session_id=session_id,
68
+ )
69
+
70
+ self._sessions[session_id] = session_state
71
+ return session_id
72
+
73
+ def get_session(self, session_id: str) -> SessionState:
74
+ """Retrieve an existing session.
75
+
76
+ Args:
77
+ session_id: Session identifier
78
+
79
+ Returns:
80
+ Python agent session state
81
+
82
+ Raises:
83
+ KeyError: If session not found
84
+ """
85
+ if session_id not in self._sessions:
86
+ raise KeyError(f"Session '{session_id}' not found")
87
+ return self._sessions[session_id]
88
+
89
+ def add_interaction(self, session_id: str, interaction: dict) -> None:
90
+ """Add an interaction to a session.
91
+
92
+ Converts the UserInteraction to an InputEvent and pushes it to the
93
+ executor's EventStream input queue. Also stores the interaction in
94
+ session state for debugging.
95
+
96
+ Args:
97
+ session_id: Session identifier
98
+ interaction: Interaction dictionary (UserInteraction format)
99
+
100
+ Raises:
101
+ KeyError: If session not found
102
+ RuntimeError: If executor not started
103
+ """
104
+ session = self.get_session(session_id)
105
+ executor = session.executor
106
+
107
+ # Store interaction in session state for debugging
108
+ session.interactions.append(interaction)
109
+
110
+ # Push to executor's EventStream if executor is running
111
+ if not executor.is_running():
112
+ raise RuntimeError("Cannot add interaction: executor not started")
113
+
114
+ # Form submission: expand to one InputEvent per sub-block (same order as agent's await_input calls)
115
+ if interaction.get("blockType") == "form" and isinstance(
116
+ interaction.get("value"), list
117
+ ):
118
+ for item in interaction["value"]:
119
+ sub_interaction = {
120
+ "blockId": item["sub_block_id"],
121
+ "blockType": item["sub_block_type"],
122
+ "value": item["value"],
123
+ }
124
+ input_event = user_interaction_to_input_event(sub_interaction)
125
+ executor.stream.push_input(input_event)
126
+ return
127
+
128
+ # Single interaction: one InputEvent
129
+ input_event = user_interaction_to_input_event(interaction)
130
+ executor.stream.push_input(input_event)
131
+
132
+ async def generate_events(self, session_id: str) -> AsyncIterator[str]:
133
+ """Generate SSE events from Python agent execution."""
134
+ session = self.get_session(session_id)
135
+ executor = session.executor
136
+ executor.start()
137
+
138
+ # Layouts that have had at least one await_user child; stream pauses on layout_end for these
139
+ layouts_with_await_user: set[str] = set()
140
+
141
+ while True:
142
+ if not executor.is_running():
143
+ async for sse in self._drain_remaining_events(session_id, executor):
144
+ yield sse
145
+ return
146
+
147
+ event = await asyncio.to_thread(executor.stream.pop_output, timeout=None)
148
+ if event is None:
149
+ continue
150
+
151
+ yield self._format_sse_event(session_id, event)
152
+
153
+ if event.block_type == "exit":
154
+ self.cleanup_session(session_id)
155
+ return
156
+
157
+ if event.message_type == "await_user":
158
+ if event.parent_block_id is not None:
159
+ layouts_with_await_user.add(event.parent_block_id)
160
+ # Do not return; keep streaming until layout_end for this layout
161
+ else:
162
+ return
163
+
164
+ if (
165
+ event.block_type == "layout_end"
166
+ and event.block_id in layouts_with_await_user
167
+ ):
168
+ return
169
+
170
+ def _format_sse_event(self, session_id: str, event: OutputEvent) -> str:
171
+ """Convert OutputEvent to SSE format and optionally log."""
172
+ event_json = output_event_to_json(event)
173
+ if self._log_events:
174
+ self._log_event_to_file(session_id, event_json)
175
+ return f"data: {json.dumps(event_json)}\n\n"
176
+
177
+ async def _drain_remaining_events(
178
+ self, session_id: str, executor: PyRuntimeExecutor
179
+ ) -> AsyncIterator[str]:
180
+ """Drain remaining events from a stopped executor."""
181
+ if executor.stream is None:
182
+ return
183
+
184
+ while executor.stream.has_output():
185
+ event = await asyncio.to_thread(executor.stream.pop_output, timeout=0.1)
186
+ if event is None:
187
+ break
188
+ yield self._format_sse_event(session_id, event)
189
+
190
+ def cleanup_session(self, session_id: str) -> None:
191
+ """Clean up session-specific resources.
192
+
193
+ Stops the executor process if running and removes the session from
194
+ internal storage.
195
+
196
+ Args:
197
+ session_id: Session identifier to clean up
198
+
199
+ Raises:
200
+ KeyError: If session not found
201
+ """
202
+ if session_id not in self._sessions:
203
+ raise KeyError(f"Session '{session_id}' not found")
204
+
205
+ session = self._sessions[session_id]
206
+
207
+ # Stop executor if it exists (terminates process)
208
+ if session.executor is not None:
209
+ session.executor.stop()
210
+
211
+ # Remove session from storage
212
+ del self._sessions[session_id]
213
+
214
+ def cleanup_all(self) -> None:
215
+ """Clean up all sessions.
216
+
217
+ Iterates through all sessions and calls cleanup_session() for each.
218
+ Called on server shutdown to ensure no orphaned processes remain.
219
+ """
220
+ # Create a list of session IDs to avoid modifying dict during iteration
221
+ session_ids = list(self._sessions.keys())
222
+
223
+ for session_id in session_ids:
224
+ with contextlib.suppress(KeyError):
225
+ self.cleanup_session(session_id)
226
+
227
+ def _log_event_to_file(self, session_id: str, event_json: dict) -> None:
228
+ """Log an event to the debug log file in JSONL format.
229
+
230
+ Args:
231
+ session_id: Session identifier
232
+ event_json: Event data as JSON-serializable dict
233
+ """
234
+ if self._events_log_file is None:
235
+ return
236
+
237
+ log_entry = {
238
+ "timestamp": datetime.datetime.now().isoformat(),
239
+ "session_id": session_id,
240
+ "event": event_json,
241
+ }
242
+
243
+ try:
244
+ with open(self._events_log_file, "a", encoding="utf-8") as f:
245
+ f.write(json.dumps(log_entry) + "\n")
246
+ except Exception as e:
247
+ logging.warning(f"Failed to write debug log: {e}")
yera/__init__.py CHANGED
@@ -1,2 +1,51 @@
1
- __version__ = "0.0.2"
1
+ from pydantic import Field as Field
2
2
 
3
+ from yera.agents import agent as agent
4
+ from yera.dsl.functions import buttons as buttons
5
+ from yera.dsl.functions import chat as chat
6
+ from yera.dsl.functions import date_picker as date_picker
7
+ from yera.dsl.functions import slider as slider
8
+ from yera.dsl.functions import struct as struct
9
+ from yera.dsl.functions import sys_prompt as sys_prompt
10
+ from yera.dsl.functions import text_input as text_input
11
+ from yera.dsl.struct import Struct as Struct
12
+ from yera.dsl.workspace import workspace as workspace
13
+ from yera.events import (
14
+ action as action,
15
+ )
16
+ from yera.events import (
17
+ bar_chart as bar_chart,
18
+ )
19
+ from yera.events import (
20
+ columns as columns,
21
+ )
22
+ from yera.events import (
23
+ container as container,
24
+ )
25
+ from yera.events import (
26
+ exit_event as exit,
27
+ )
28
+ from yera.events import (
29
+ form as form,
30
+ )
31
+ from yera.events import (
32
+ line_chart as line_chart,
33
+ )
34
+ from yera.events import (
35
+ markdown as markdown,
36
+ )
37
+ from yera.events import (
38
+ quit_event as quit,
39
+ )
40
+ from yera.events import (
41
+ spinner as spinner,
42
+ )
43
+ from yera.events import (
44
+ table as table,
45
+ )
46
+ from yera.models.llm_atlas_proxy import llm as llm
47
+ from yera.tools.decorator import tool as tool
48
+ from yera.tools.tool_atlas import get_tool_atlas as get_tool_atlas
49
+ from yera.tools.tool_utils import save_tool as save_tool
50
+
51
+ __all__ = ["Struct", "agent", "exit", "quit"]
@@ -0,0 +1,2 @@
1
+ from .context import get_agent_context as get_agent_context
2
+ from .decorator import agent as agent
yera/agents/context.py ADDED
@@ -0,0 +1,41 @@
1
+ from contextvars import ContextVar
2
+ from typing import Optional
3
+
4
+ from yera.agents.dataclasses import AgentMetadata
5
+
6
+ _current_agent_context: ContextVar[Optional["AgentContext"]] = ContextVar(
7
+ "agent_context", default=None
8
+ )
9
+
10
+
11
+ class AgentContext:
12
+ def __init__(self, metadata: AgentMetadata):
13
+ self.metadata = metadata
14
+ self.instance_number = 0
15
+ self._token = None
16
+ self._graph = None
17
+ self._is_top_of_agent_stack = None
18
+
19
+ def __enter__(self):
20
+ self._is_top_of_agent_stack = _current_agent_context.get() is None
21
+ self._token = _current_agent_context.set(self)
22
+ return self
23
+
24
+ def __exit__(self, exc_type, exc_val, exc_tb):
25
+ _current_agent_context.reset(self._token)
26
+
27
+ def top_level(self) -> bool:
28
+ if self._is_top_of_agent_stack is None:
29
+ raise RuntimeError("This context has not been initialised")
30
+ return self._is_top_of_agent_stack
31
+
32
+
33
+ def no_active_agent_context():
34
+ return _current_agent_context.get() is None
35
+
36
+
37
+ def get_agent_context():
38
+ agent = _current_agent_context.get()
39
+ if agent is None:
40
+ raise RuntimeError("No agent context")
41
+ return agent
@@ -0,0 +1,69 @@
1
+ from dataclasses import dataclass
2
+ from inspect import Parameter
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class AgentInput:
8
+ name: str
9
+ type: str
10
+ kind: str
11
+ has_default: bool
12
+ default_value: Any
13
+
14
+ @staticmethod
15
+ def from_parameter(p: Parameter) -> "AgentInput":
16
+ return AgentInput(
17
+ name=p.name,
18
+ type=str(p.annotation),
19
+ kind=str(p.kind),
20
+ has_default=p.default is not Parameter.empty,
21
+ default_value=None if p.default is Parameter.empty else p.default,
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class AgentInstance:
27
+ agent_id: str
28
+ instance_id: int
29
+
30
+
31
+ @dataclass
32
+ class AgentMetadata:
33
+ name: str
34
+ module: str
35
+ identifier: str
36
+ params: list[AgentInput]
37
+ return_type: str
38
+ docs: str | None = None
39
+ description: str | None = None
40
+
41
+ def make_instance_id(self, ix: int) -> AgentInstance:
42
+ return AgentInstance(
43
+ agent_id=f"{self.module}.{self.identifier}", instance_id=ix
44
+ )
45
+
46
+ def pretty_print(self) -> str:
47
+ lines = [
48
+ f"Agent: {self.name}",
49
+ f"Module: {self.module}",
50
+ f"Identifier: {self.identifier}",
51
+ ]
52
+
53
+ if self.description:
54
+ lines.append(f"Description: {self.description}")
55
+
56
+ if self.docs:
57
+ lines.append(f"Docs: {self.docs}")
58
+
59
+ lines.append(f"\nParameters ({len(self.params)}):")
60
+ if self.params:
61
+ for param in self.params:
62
+ default = f" = {param.default_value}" if param.has_default else ""
63
+ lines.append(f" - {param.name}: {param.type}{default}")
64
+ else:
65
+ lines.append(" (none)")
66
+
67
+ lines.append(f"\nReturn Type: {self.return_type}")
68
+
69
+ return "\n".join(lines)
@@ -0,0 +1,207 @@
1
+ import functools
2
+ import inspect
3
+ import traceback
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, get_type_hints
6
+
7
+ import cloudpickle
8
+
9
+ from yera.agents.context import (
10
+ AgentContext,
11
+ no_active_agent_context,
12
+ )
13
+ from yera.agents.dataclasses import AgentInput, AgentMetadata
14
+ from yera.agents.typing.coerce import (
15
+ coerce_value,
16
+ )
17
+ from yera.agents.typing.utils import get_type_name
18
+ from yera.agents.typing.validate import assert_value_is_return_type, validate_signature
19
+ from yera.dsl.functions import sys_prompt
20
+ from yera.events import exit_event
21
+ from yera.events.runtime import PyRuntimeExecutor, stream_handler
22
+ from yera.events.stream import EventStream
23
+ from yera.models.llm_atlas_proxy import llm
24
+
25
+ if TYPE_CHECKING:
26
+ from yera import LLMContext
27
+
28
+
29
+ class AgentFunctionWrapper:
30
+ def __init__(
31
+ self,
32
+ agent_function: Callable,
33
+ name: str | None,
34
+ description: str | None,
35
+ model: "LLMContext | None",
36
+ sys_prompt_str: str | None,
37
+ ):
38
+ fn_name = agent_function.__name__
39
+ module = agent_function.__module__
40
+ identifier = f"{module}.{fn_name}"
41
+
42
+ self.agent_function = agent_function
43
+ self.model = model or llm.get_default()
44
+ self.sys_prompt = sys_prompt_str
45
+
46
+ sig = inspect.signature(agent_function)
47
+ type_hints = get_type_hints(agent_function)
48
+ validate_signature(sig, type_hints, identifier)
49
+ self.signature = sig
50
+ self.type_hints = type_hints
51
+
52
+ params = _get_agent_inputs(sig, type_hints)
53
+
54
+ self.return_cls = _get_return_type(sig, type_hints)
55
+ self.metadata = AgentMetadata(
56
+ name=name or fn_name,
57
+ description=description,
58
+ module=module,
59
+ docs=agent_function.__doc__,
60
+ identifier=identifier,
61
+ params=params,
62
+ return_type=get_type_name(self.return_cls),
63
+ )
64
+
65
+ functools.update_wrapper(self, agent_function)
66
+ self.__signature__ = sig
67
+
68
+ def _validate_and_coerce_args(self, args: tuple, kwargs: dict) -> dict:
69
+ try:
70
+ bound = self.signature.bind(*args, **kwargs)
71
+ except TypeError as e:
72
+ raise TypeError(
73
+ f"Invalid arguments for {self.name} ({self.metadata.identifier}): {e}"
74
+ ) from None
75
+
76
+ bound.apply_defaults()
77
+
78
+ coerced_args = {}
79
+
80
+ for name, arg in bound.arguments.items():
81
+ if arg is None:
82
+ continue
83
+
84
+ try:
85
+ coerced_args[name] = coerce_value(arg, self.type_hints[name])
86
+ except (TypeError, ValueError) as e:
87
+ raise TypeError(
88
+ f"Argument '{name}' for agent '{self.metadata.identifier}': "
89
+ f"expected {get_type_name(self.type_hints[name])}, "
90
+ f"got {type(arg).__name__}. Error: {e}"
91
+ ) from None
92
+
93
+ return coerced_args
94
+
95
+ def invoke(self, *args, **kwargs):
96
+ with AgentContext(self.metadata) as ctx:
97
+ model_entered = True
98
+
99
+ try:
100
+ c_args = self._validate_and_coerce_args(args, kwargs)
101
+
102
+ if self.model and ctx.top_level():
103
+ self.model.__enter__()
104
+ model_entered = True
105
+
106
+ if self.sys_prompt:
107
+ sys_prompt(self.sys_prompt)
108
+
109
+ ret_value = self.agent_function(**c_args)
110
+
111
+ assert_value_is_return_type(ret_value, self.return_cls, self.metadata)
112
+
113
+ if ctx.top_level():
114
+ exit_event(
115
+ exit_code=0,
116
+ reason="Yera program completed successfully.",
117
+ return_value=ret_value,
118
+ )
119
+ return ret_value
120
+ except Exception as e:
121
+ if ctx.top_level():
122
+ exit_event(
123
+ exit_code=1,
124
+ reason=f"Error during agent execution: {e!s}",
125
+ return_value=traceback.format_exc(),
126
+ )
127
+ else:
128
+ raise
129
+ finally:
130
+ if model_entered and self.model and ctx.top_level():
131
+ self.model.__exit__(None, None, None)
132
+
133
+ @property
134
+ def name(self) -> str:
135
+ return self.metadata.name
136
+
137
+ def __call__(self, *args, **kwargs):
138
+ if len(kwargs) != 0:
139
+ raise NotImplementedError("Agent kwargs are currently not supported.")
140
+
141
+ EventStream.build()
142
+
143
+ if no_active_agent_context(): # top of the agent stack, about to start
144
+ with PyRuntimeExecutor(self, args):
145
+ exit_evt = stream_handler(self.metadata)
146
+
147
+ if exit_evt.data.exit_code != 0:
148
+ raise RuntimeError(
149
+ f"Yera program exited with code: {exit_evt.data.exit_code},\n"
150
+ f"AGENT: {exit_evt.agent_instance.agent_id}\n"
151
+ f"REASON: {exit_evt.data.reason}\n"
152
+ f"TRACEBACK: {exit_evt.data.return_value}\n"
153
+ )
154
+
155
+ return coerce_value(exit_evt.data.return_value, self.return_cls)
156
+ return self.invoke(*args)
157
+
158
+ def __reduce__(self):
159
+ args = (
160
+ self.metadata.name,
161
+ self.metadata.description,
162
+ self.model,
163
+ self.sys_prompt,
164
+ )
165
+ return (
166
+ _unpickle_agent_wrapper,
167
+ (cloudpickle.dumps(self.agent_function), args),
168
+ )
169
+
170
+
171
+ def _get_return_type(sig, type_hints):
172
+ ret_type = type_hints.get("return", None)
173
+ if ret_type is None and sig.return_annotation is not inspect.Signature.empty:
174
+ ret_type = sig.return_annotation
175
+ return ret_type
176
+
177
+
178
+ def _get_agent_inputs(sig, type_hints) -> list[AgentInput]:
179
+ params = []
180
+ for param_name, param in sig.parameters.items():
181
+ if param_name in type_hints:
182
+ param = param.replace(annotation=type_hints[param_name])
183
+ params.append(AgentInput.from_parameter(param))
184
+ return params
185
+
186
+
187
+ def _unpickle_agent_wrapper(pickled_func, args):
188
+ agent_function = cloudpickle.loads(pickled_func)
189
+ return AgentFunctionWrapper(agent_function, *args)
190
+
191
+
192
+ def agent(
193
+ fn=None,
194
+ *,
195
+ name=None,
196
+ description=None,
197
+ model=None,
198
+ sys_prompt=None,
199
+ ) -> AgentFunctionWrapper:
200
+ args = (name, description, model, sys_prompt)
201
+
202
+ def decorator(func) -> AgentFunctionWrapper:
203
+ return AgentFunctionWrapper(func, *args)
204
+
205
+ if fn is not None and callable(fn):
206
+ return decorator(fn)
207
+ return decorator