agentex-sdk 0.2.0__py3-none-any.whl → 0.2.2__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 (36) hide show
  1. agentex/_version.py +1 -1
  2. agentex/lib/adk/_modules/acp.py +2 -1
  3. agentex/lib/adk/_modules/agent_task_tracker.py +2 -1
  4. agentex/lib/adk/_modules/agents.py +2 -1
  5. agentex/lib/adk/_modules/events.py +2 -1
  6. agentex/lib/adk/_modules/messages.py +4 -3
  7. agentex/lib/adk/_modules/state.py +2 -1
  8. agentex/lib/adk/_modules/streaming.py +4 -3
  9. agentex/lib/adk/_modules/tasks.py +2 -1
  10. agentex/lib/adk/_modules/tracing.py +2 -1
  11. agentex/lib/adk/providers/_modules/litellm.py +2 -2
  12. agentex/lib/adk/providers/_modules/openai.py +2 -2
  13. agentex/lib/adk/utils/_modules/client.py +12 -0
  14. agentex/lib/cli/commands/init.py +8 -4
  15. agentex/lib/cli/templates/default/README.md.j2 +23 -2
  16. agentex/lib/cli/templates/default/dev.ipynb.j2 +126 -0
  17. agentex/lib/cli/templates/sync/README.md.j2 +22 -2
  18. agentex/lib/cli/templates/sync/dev.ipynb.j2 +167 -0
  19. agentex/lib/cli/templates/sync/project/acp.py.j2 +63 -14
  20. agentex/lib/cli/templates/temporal/README.md.j2 +24 -3
  21. agentex/lib/cli/templates/temporal/dev.ipynb.j2 +126 -0
  22. agentex/lib/core/adapters/streams/adapter_redis.py +4 -4
  23. agentex/lib/core/adapters/streams/port.py +1 -1
  24. agentex/lib/core/services/adk/streaming.py +2 -3
  25. agentex/lib/core/temporal/activities/__init__.py +2 -2
  26. agentex/lib/sdk/fastacp/base/base_acp_server.py +11 -2
  27. agentex/lib/utils/dev_tools/__init__.py +9 -0
  28. agentex/lib/utils/dev_tools/async_messages.py +386 -0
  29. agentex/resources/agents.py +511 -3
  30. agentex/resources/tasks.py +4 -4
  31. agentex/types/agent_rpc_response.py +32 -4
  32. {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.2.dist-info}/METADATA +1 -1
  33. {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.2.dist-info}/RECORD +36 -30
  34. {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.2.dist-info}/WHEEL +0 -0
  35. {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.2.dist-info}/entry_points.txt +0 -0
  36. {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,386 @@
1
+ """
2
+ Development utility for subscribing to async task messages with streaming support.
3
+
4
+ This module provides utilities to read existing messages from a task and subscribe
5
+ to new streaming messages, handling mid-stream connections gracefully.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime, timezone
10
+ from typing import List, Optional
11
+
12
+ from yaspin.core import Yaspin
13
+
14
+ from agentex import Agentex
15
+ from agentex.types import Task, TaskMessage, TextContent, ToolRequestContent, ToolResponseContent
16
+ from agentex.types.task_message_update import (
17
+ TaskMessageUpdate,
18
+ StreamTaskMessageStart,
19
+ StreamTaskMessageDelta,
20
+ StreamTaskMessageFull,
21
+ StreamTaskMessageDone
22
+ )
23
+ from agentex.types.text_delta import TextDelta
24
+
25
+ from rich.console import Console
26
+ from rich.panel import Panel
27
+ from rich.markdown import Markdown
28
+ from yaspin import yaspin
29
+
30
+
31
+ def print_task_message(
32
+ message: TaskMessage,
33
+ print_messages: bool = True,
34
+ rich_print: bool = True,
35
+ ) -> None:
36
+ """
37
+ Print a task message in a formatted way.
38
+
39
+ Args:
40
+ message: The task message to print
41
+ print_messages: Whether to actually print the message (for debugging)
42
+ rich_print: Whether to use rich to print the message
43
+ """
44
+ if not print_messages:
45
+ return
46
+
47
+ # Skip empty messages
48
+ if isinstance(message.content, TextContent) and not message.content.content.strip():
49
+ return
50
+
51
+ timestamp = message.created_at.strftime("%m/%d/%Y %H:%M:%S") if message.created_at else "N/A"
52
+
53
+ console = None
54
+ if rich_print:
55
+ console = Console(width=80) # Fit better in Jupyter cells
56
+
57
+ if isinstance(message.content, TextContent):
58
+ content = message.content.content
59
+ content_type = "text"
60
+ elif isinstance(message.content, ToolRequestContent):
61
+ tool_name = message.content.name
62
+ tool_args = message.content.arguments
63
+
64
+ # Format arguments as pretty JSON
65
+ try:
66
+ if isinstance(tool_args, str):
67
+ parsed_args = json.loads(tool_args)
68
+ formatted_args = json.dumps(parsed_args, indent=2)
69
+ else:
70
+ formatted_args = json.dumps(tool_args, indent=2)
71
+ content = f"🔧 **Tool Request: {tool_name}**\n\n**Arguments:**\n```json\n{formatted_args}\n```"
72
+ except (json.JSONDecodeError, TypeError):
73
+ content = f"🔧 **Tool Request: {tool_name}**\n\n**Arguments:**\n```json\n{tool_args}\n```"
74
+
75
+ content_type = "tool_request"
76
+ elif isinstance(message.content, ToolResponseContent):
77
+ tool_name = message.content.name
78
+ tool_response = message.content.content
79
+
80
+ # Try to parse and format JSON response nicely
81
+ try:
82
+ if isinstance(tool_response, str):
83
+ parsed_response = json.loads(tool_response)
84
+ formatted_json = json.dumps(parsed_response, indent=2)
85
+ content = f"✅ **Tool Response: {tool_name}**\n\n**Response:**\n```json\n{formatted_json}\n```"
86
+ else:
87
+ formatted_json = json.dumps(tool_response, indent=2)
88
+ content = f"✅ **Tool Response: {tool_name}**\n\n**Response:**\n```json\n{formatted_json}\n```"
89
+ except (json.JSONDecodeError, TypeError):
90
+ # If it's not valid JSON, display as text
91
+ if isinstance(tool_response, str):
92
+ # Try to extract text content if it's a JSON string with text field
93
+ try:
94
+ parsed = json.loads(tool_response)
95
+ if isinstance(parsed, dict) and "text" in parsed:
96
+ text_content = str(parsed["text"])
97
+ content = f"✅ **Tool Response: {tool_name}**\n\n{text_content}"
98
+ else:
99
+ content = f"✅ **Tool Response: {tool_name}**\n\n{tool_response}"
100
+ except json.JSONDecodeError:
101
+ content = f"✅ **Tool Response: {tool_name}**\n\n{tool_response}"
102
+ else:
103
+ content = f"✅ **Tool Response: {tool_name}**\n\n{tool_response}"
104
+
105
+ content_type = "tool_response"
106
+ else:
107
+ content = f"{type(message.content).__name__}: {message.content}"
108
+ content_type = "other"
109
+
110
+ if rich_print and console:
111
+ author_color = "bright_cyan" if message.content.author == "user" else "green"
112
+ title = f"[bold {author_color}]{message.content.author.upper()}[/bold {author_color}] [{timestamp}]"
113
+
114
+ # Use different border styles for tool messages
115
+ if content_type == "tool_request":
116
+ border_style = "yellow"
117
+ elif content_type == "tool_response":
118
+ border_style = "bright_green"
119
+ else:
120
+ border_style = author_color
121
+
122
+ panel = Panel(Markdown(content), title=title, border_style=border_style, width=80)
123
+ console.print(panel)
124
+ else:
125
+ title = f"{message.content.author.upper()} [{timestamp}]"
126
+ print(f"{title}\n{content}\n")
127
+
128
+
129
+ def print_task_message_update(
130
+ task_message_update: TaskMessageUpdate,
131
+ print_messages: bool = True,
132
+ rich_print: bool = True,
133
+ show_deltas: bool = True,
134
+ ) -> None:
135
+ """
136
+ Print a task message update in a formatted way.
137
+
138
+ This function handles different types of TaskMessageUpdate objects:
139
+ - StreamTaskMessageStart: Shows start indicator
140
+ - StreamTaskMessageDelta: Shows deltas in real-time (if show_deltas=True)
141
+ - StreamTaskMessageFull: Shows complete message content
142
+ - StreamTaskMessageDone: Shows completion indicator
143
+
144
+ Args:
145
+ task_message_update: The TaskMessageUpdate object to print
146
+ print_messages: Whether to actually print the message (for debugging)
147
+ rich_print: Whether to use rich formatting
148
+ show_deltas: Whether to show delta updates in real-time
149
+ """
150
+ if not print_messages:
151
+ return
152
+
153
+ console = None
154
+ if rich_print:
155
+ console = Console(width=80)
156
+
157
+ if isinstance(task_message_update, StreamTaskMessageStart):
158
+ if rich_print and console:
159
+ console.print("🚀 [cyan]Agent started responding...[/cyan]")
160
+ else:
161
+ print("🚀 Agent started responding...")
162
+
163
+ elif isinstance(task_message_update, StreamTaskMessageDelta):
164
+ if show_deltas and task_message_update.delta:
165
+ if isinstance(task_message_update.delta, TextDelta):
166
+ print(task_message_update.delta.text_delta, end="", flush=True)
167
+ elif rich_print and console:
168
+ console.print(f"[yellow]Non-text delta: {type(task_message_update.delta).__name__}[/yellow]")
169
+ else:
170
+ print(f"Non-text delta: {type(task_message_update.delta).__name__}")
171
+
172
+ elif isinstance(task_message_update, StreamTaskMessageFull):
173
+ if isinstance(task_message_update.content, TextContent):
174
+ timestamp = datetime.now().strftime("%m/%d/%Y %H:%M:%S")
175
+
176
+ if rich_print and console:
177
+ author_color = "bright_cyan" if task_message_update.content.author == "user" else "green"
178
+ title = f"[bold {author_color}]{task_message_update.content.author.upper()}[/bold {author_color}] [{timestamp}]"
179
+ panel = Panel(Markdown(task_message_update.content.content), title=title, border_style=author_color, width=80)
180
+ console.print(panel)
181
+ else:
182
+ title = f"{task_message_update.content.author.upper()} [{timestamp}]"
183
+ print(f"\n{title}\n{task_message_update.content.content}\n")
184
+ else:
185
+ content_type = type(task_message_update.content).__name__
186
+ if rich_print and console:
187
+ console.print(f"[yellow]Non-text content: {content_type}[/yellow]")
188
+ else:
189
+ print(f"Non-text content: {content_type}")
190
+
191
+ else: # StreamTaskMessageDone
192
+ if rich_print and console:
193
+ console.print("\n✅ [green]Agent finished responding.[/green]")
194
+ else:
195
+ print("\n✅ Agent finished responding.")
196
+
197
+
198
+ def subscribe_to_async_task_messages(
199
+ client: Agentex,
200
+ task: Task,
201
+ only_after_timestamp: Optional[datetime] = None,
202
+ print_messages: bool = True,
203
+ rich_print: bool = True,
204
+ timeout: int = 10,
205
+ ) -> List[TaskMessage]:
206
+ """
207
+ Subscribe to async task messages and collect completed messages.
208
+
209
+ This function:
210
+ 1. Reads all existing messages from the task
211
+ 2. Optionally filters messages after a timestamp
212
+ 3. Shows a loading message while listening
213
+ 4. Subscribes to task message events
214
+ 5. Fetches and displays complete messages when they finish
215
+ 6. Returns all messages collected during the session
216
+
217
+ Features:
218
+ - Uses Rich library for beautiful formatting in Jupyter notebooks
219
+ - Agent messages are formatted as Markdown
220
+ - User and agent messages are displayed in colored panels with fixed width
221
+ - Optimized for Jupyter notebook display
222
+
223
+ Args:
224
+ client: The Agentex client instance
225
+ task: The task to subscribe to
226
+ print_messages: Whether to print messages as they arrive
227
+ only_after_timestamp: Only include messages created after this timestamp. If None, all messages will be included.
228
+ rich_print: Whether to use rich to print the message
229
+ timeout: The timeout in seconds for the streaming connection. If the connection times out, the function will return with any messages collected so far.
230
+ Returns:
231
+ List of TaskMessage objects collected during the session
232
+
233
+ Raises:
234
+ ValueError: If the task doesn't have a name (required for streaming)
235
+ """
236
+
237
+ messages_to_return: List[TaskMessage] = []
238
+
239
+ # Read existing messages
240
+ messages = []
241
+ try:
242
+ # List all messages for this task - MessageListResponse is just a List[TaskMessage]
243
+ messages = client.messages.list(task_id=task.id)
244
+
245
+ except Exception as e:
246
+ print(f"Error reading existing messages: {e}")
247
+
248
+ # Filter and display existing messages
249
+ for message in messages:
250
+ if only_after_timestamp:
251
+ if message.created_at is not None:
252
+ # Handle timezone comparison - make both datetimes timezone-aware
253
+ message_time = message.created_at
254
+ if message_time.tzinfo is None:
255
+ # If message time is naive, assume it's in UTC
256
+ message_time = message_time.replace(tzinfo=timezone.utc)
257
+
258
+ comparison_time = only_after_timestamp
259
+ if comparison_time.tzinfo is None:
260
+ # If comparison time is naive, assume it's in UTC
261
+ comparison_time = comparison_time.replace(tzinfo=timezone.utc)
262
+
263
+ if message_time < comparison_time:
264
+ continue
265
+ else:
266
+ messages_to_return.append(message)
267
+ print_task_message(message, print_messages, rich_print)
268
+ else:
269
+ messages_to_return.append(message)
270
+ print_task_message(message, print_messages, rich_print)
271
+
272
+ # Subscribe to server-side events using tasks.stream_events_by_name
273
+ # This is the proper way to get agent responses after sending an event in agentic agents
274
+
275
+ # Ensure task has a name
276
+ if not task.name:
277
+ print("Error: Task must have a name to use stream_events_by_name")
278
+ raise ValueError("Task name is required")
279
+
280
+ try:
281
+ # Use stream_events_by_name to subscribe to TaskMessageUpdate events for this task
282
+ # This doesn't require knowing the agent_id, just the task name
283
+
284
+ # Track active streaming spinners per message index
285
+ active_spinners: dict[int, Yaspin] = {} # index -> yaspin spinner object
286
+
287
+ with client.tasks.with_streaming_response.stream_events_by_name(
288
+ task_name=task.name,
289
+ timeout=timeout
290
+ ) as response:
291
+
292
+ try:
293
+ for task_message_update_str in response.iter_text():
294
+ try:
295
+ # Parse SSE format
296
+ if task_message_update_str.strip().startswith('data: '):
297
+ task_message_update_json = task_message_update_str.strip()[6:] # Remove 'data: ' prefix
298
+ task_message_update_data = json.loads(task_message_update_json)
299
+
300
+ # Deserialize the discriminated union TaskMessageUpdate based on the "type" field
301
+ message_type = task_message_update_data.get("type", "unknown")
302
+
303
+ # Handle different message types for streaming progress
304
+ if message_type == "start":
305
+ task_message_update = StreamTaskMessageStart.model_validate(task_message_update_data)
306
+ index = task_message_update.index or 0
307
+
308
+ # Start a yaspin spinner for this message
309
+ if print_messages and index not in active_spinners:
310
+ spinner = yaspin(text="🔄 Agent responding...")
311
+ spinner.start()
312
+ active_spinners[index] = spinner
313
+
314
+ elif message_type == "delta":
315
+ task_message_update = StreamTaskMessageDelta.model_validate(task_message_update_data)
316
+ index = task_message_update.index or 0
317
+
318
+ # Spinner continues running (no update needed for HTML) or if spinner has not been created yet, create it
319
+ if print_messages and index not in active_spinners:
320
+ spinner = yaspin(text="🔄 Agent responding...")
321
+ spinner.start()
322
+ active_spinners[index] = spinner
323
+
324
+ elif message_type == "full":
325
+ task_message_update = StreamTaskMessageFull.model_validate(task_message_update_data)
326
+ index = task_message_update.index or 0
327
+
328
+ # Stop spinner and show message
329
+ if index in active_spinners:
330
+ active_spinners[index].stop()
331
+ del active_spinners[index]
332
+
333
+ if task_message_update.parent_task_message and task_message_update.parent_task_message.id:
334
+ finished_message = client.messages.retrieve(task_message_update.parent_task_message.id)
335
+ messages_to_return.append(finished_message)
336
+ print_task_message(finished_message, print_messages, rich_print)
337
+
338
+ elif message_type == "done":
339
+ task_message_update = StreamTaskMessageDone.model_validate(task_message_update_data)
340
+ index = task_message_update.index or 0
341
+
342
+ # Stop spinner and show message
343
+ if index in active_spinners:
344
+ active_spinners[index].stop()
345
+ del active_spinners[index]
346
+
347
+ if task_message_update.parent_task_message and task_message_update.parent_task_message.id:
348
+ finished_message = client.messages.retrieve(task_message_update.parent_task_message.id)
349
+ messages_to_return.append(finished_message)
350
+ print_task_message(finished_message, print_messages, rich_print)
351
+
352
+ # Ignore "connected" message type
353
+ elif message_type == "connected":
354
+ pass
355
+ else:
356
+ if print_messages:
357
+ print(f"Unknown TaskMessageUpdate type: {message_type}")
358
+
359
+ except json.JSONDecodeError:
360
+ # Skip invalid JSON or SSE metadata lines
361
+ if task_message_update_str.strip() and not task_message_update_str.startswith(':'):
362
+ if print_messages:
363
+ print(f"Skipping non-JSON: {task_message_update_str.strip()}")
364
+ continue
365
+ except Exception as e:
366
+ if print_messages:
367
+ print(f"Error processing TaskMessageUpdate: {e}")
368
+ print(f"Raw data: {task_message_update_str.strip()}")
369
+ continue
370
+ finally:
371
+ # Stop any remaining spinners when we're done
372
+ for spinner in active_spinners.values():
373
+ spinner.stop()
374
+ active_spinners.clear()
375
+
376
+ except Exception as e:
377
+ # Handle timeout gracefully
378
+ if "timeout" in str(e).lower() or "timed out" in str(e).lower():
379
+ if print_messages:
380
+ print(f"Streaming timed out after {timeout} seconds - returning collected messages")
381
+ else:
382
+ if print_messages:
383
+ print(f"Error subscribing to events: {e}")
384
+ print("Make sure your agent is running and the task exists")
385
+
386
+ return messages_to_return