agentex-sdk 0.2.0__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.
- agentex/_version.py +1 -1
- agentex/lib/adk/_modules/messages.py +2 -2
- agentex/lib/adk/_modules/streaming.py +2 -2
- agentex/lib/adk/providers/_modules/litellm.py +2 -2
- agentex/lib/adk/providers/_modules/openai.py +2 -2
- agentex/lib/cli/commands/init.py +8 -4
- agentex/lib/cli/templates/default/README.md.j2 +23 -2
- agentex/lib/cli/templates/default/dev.ipynb.j2 +127 -0
- agentex/lib/cli/templates/sync/README.md.j2 +22 -2
- agentex/lib/cli/templates/sync/dev.ipynb.j2 +181 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +63 -14
- agentex/lib/cli/templates/temporal/README.md.j2 +24 -3
- agentex/lib/cli/templates/temporal/dev.ipynb.j2 +127 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +4 -4
- agentex/lib/core/adapters/streams/port.py +1 -1
- agentex/lib/core/services/adk/streaming.py +2 -3
- agentex/lib/core/temporal/activities/__init__.py +2 -2
- agentex/lib/utils/dev_tools/__init__.py +9 -0
- agentex/lib/utils/dev_tools/async_messages.py +386 -0
- {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.1.dist-info}/METADATA +1 -1
- {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.1.dist-info}/RECORD +24 -19
- {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.1.dist-info}/WHEEL +0 -0
- {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.1.dist-info}/entry_points.txt +0 -0
- {agentex_sdk-0.2.0.dist-info → agentex_sdk-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
{
|
2
|
+
"cells": [
|
3
|
+
{
|
4
|
+
"cell_type": "code",
|
5
|
+
"execution_count": null,
|
6
|
+
"id": "36834357",
|
7
|
+
"metadata": {},
|
8
|
+
"outputs": [],
|
9
|
+
"source": [
|
10
|
+
"from agentex import Agentex\n",
|
11
|
+
"\n",
|
12
|
+
"client = Agentex(base_url=\"http://localhost:5003\")"
|
13
|
+
]
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"cell_type": "code",
|
17
|
+
"execution_count": null,
|
18
|
+
"id": "d1c309d6",
|
19
|
+
"metadata": {},
|
20
|
+
"outputs": [],
|
21
|
+
"source": [
|
22
|
+
"AGENT_NAME = \"{{ agent_name }}\""
|
23
|
+
]
|
24
|
+
},
|
25
|
+
{
|
26
|
+
"cell_type": "code",
|
27
|
+
"execution_count": null,
|
28
|
+
"id": "9f6e6ef0",
|
29
|
+
"metadata": {},
|
30
|
+
"outputs": [],
|
31
|
+
"source": [
|
32
|
+
"# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n",
|
33
|
+
"\n",
|
34
|
+
"from typing import cast\n",
|
35
|
+
"import uuid\n",
|
36
|
+
"\n",
|
37
|
+
"from agentex.types import Task\n",
|
38
|
+
"\n",
|
39
|
+
"TASK_ID = str(uuid.uuid4())[:8]\n",
|
40
|
+
"\n",
|
41
|
+
"rpc_response = client.agents.rpc_by_name(\n",
|
42
|
+
" agent_name=AGENT_NAME,\n",
|
43
|
+
" method=\"task/create\",\n",
|
44
|
+
" params={\n",
|
45
|
+
" \"name\": f\"{TASK_ID}-task\",\n",
|
46
|
+
" \"params\": {}\n",
|
47
|
+
" }\n",
|
48
|
+
")\n",
|
49
|
+
"\n",
|
50
|
+
"task = cast(Task, rpc_response.result)\n",
|
51
|
+
"print(task)"
|
52
|
+
]
|
53
|
+
},
|
54
|
+
{
|
55
|
+
"cell_type": "code",
|
56
|
+
"execution_count": null,
|
57
|
+
"id": "b03b0d37",
|
58
|
+
"metadata": {},
|
59
|
+
"outputs": [],
|
60
|
+
"source": [
|
61
|
+
"# Test non streaming response\n",
|
62
|
+
"from typing import cast\n",
|
63
|
+
"from agentex.types import Event\n",
|
64
|
+
"\n",
|
65
|
+
"# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n",
|
66
|
+
"# - TextContent: A message with just text content \n",
|
67
|
+
"# - DataContent: A message with JSON-serializable data content\n",
|
68
|
+
"# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n",
|
69
|
+
"# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n",
|
70
|
+
"\n",
|
71
|
+
"# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n",
|
72
|
+
"\n",
|
73
|
+
"rpc_response = client.agents.rpc_by_name(\n",
|
74
|
+
" agent_name=AGENT_NAME,\n",
|
75
|
+
" method=\"event/send\",\n",
|
76
|
+
" params={\n",
|
77
|
+
" \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n",
|
78
|
+
" \"task_id\": task.id,\n",
|
79
|
+
" }\n",
|
80
|
+
")\n",
|
81
|
+
"\n",
|
82
|
+
"event = cast(Event, rpc_response.result)\n",
|
83
|
+
"print(event)"
|
84
|
+
]
|
85
|
+
},
|
86
|
+
{
|
87
|
+
"cell_type": "code",
|
88
|
+
"execution_count": null,
|
89
|
+
"id": "a6927cc0",
|
90
|
+
"metadata": {},
|
91
|
+
"outputs": [],
|
92
|
+
"source": [
|
93
|
+
"from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n",
|
94
|
+
"\n",
|
95
|
+
"task_messages = subscribe_to_async_task_messages(\n",
|
96
|
+
" client=client,\n",
|
97
|
+
" task=task, \n",
|
98
|
+
" only_after_timestamp=event.created_at, \n",
|
99
|
+
" print_messages=True,\n",
|
100
|
+
" rich_print=True,\n",
|
101
|
+
" timeout=5,\n",
|
102
|
+
")"
|
103
|
+
]
|
104
|
+
}
|
105
|
+
],
|
106
|
+
"metadata": {
|
107
|
+
"kernelspec": {
|
108
|
+
"display_name": ".venv",
|
109
|
+
"language": "python",
|
110
|
+
"name": "python3"
|
111
|
+
},
|
112
|
+
"language_info": {
|
113
|
+
"codemirror_mode": {
|
114
|
+
"name": "ipython",
|
115
|
+
"version": 3
|
116
|
+
},
|
117
|
+
"file_extension": ".py",
|
118
|
+
"mimetype": "text/x-python",
|
119
|
+
"name": "python",
|
120
|
+
"nbconvert_exporter": "python",
|
121
|
+
"pygments_lexer": "ipython3",
|
122
|
+
"version": "3.12.9"
|
123
|
+
}
|
124
|
+
},
|
125
|
+
"nbformat": 4,
|
126
|
+
"nbformat_minor": 5
|
127
|
+
}
|
@@ -7,13 +7,13 @@ from typing import Annotated, Any
|
|
7
7
|
import redis.asyncio as redis
|
8
8
|
from fastapi import Depends
|
9
9
|
|
10
|
-
from agentex.lib.core.adapters.streams.port import
|
10
|
+
from agentex.lib.core.adapters.streams.port import StreamRepository
|
11
11
|
from agentex.lib.utils.logging import make_logger
|
12
12
|
|
13
13
|
logger = make_logger(__name__)
|
14
14
|
|
15
15
|
|
16
|
-
class
|
16
|
+
class RedisStreamRepository(StreamRepository):
|
17
17
|
"""
|
18
18
|
A simplified Redis implementation of the EventStreamRepository interface.
|
19
19
|
Optimized for text/JSON streaming with SSE.
|
@@ -123,6 +123,6 @@ class RedisEventStreamRepository(EventStreamRepository):
|
|
123
123
|
raise
|
124
124
|
|
125
125
|
|
126
|
-
|
127
|
-
|
126
|
+
DRedisStreamRepository = Annotated[
|
127
|
+
RedisStreamRepository | None, Depends(RedisStreamRepository)
|
128
128
|
]
|
@@ -2,7 +2,7 @@ import json
|
|
2
2
|
from typing import Literal, cast
|
3
3
|
|
4
4
|
from agentex import AsyncAgentex
|
5
|
-
from agentex.lib.core.adapters.streams.port import
|
5
|
+
from agentex.lib.core.adapters.streams.port import StreamRepository
|
6
6
|
from agentex.lib.types.task_message_updates import (
|
7
7
|
TaskMessageDelta,
|
8
8
|
TaskMessageUpdate,
|
@@ -22,7 +22,6 @@ from agentex.types.task_message import (
|
|
22
22
|
TaskMessage,
|
23
23
|
TaskMessageContent,
|
24
24
|
)
|
25
|
-
from agentex.types.task_message_content_param import TaskMessageContentParam
|
26
25
|
from agentex.types.text_content import TextContent
|
27
26
|
from agentex.types.tool_request_content import ToolRequestContent
|
28
27
|
from agentex.types.tool_response_content import ToolResponseContent
|
@@ -221,7 +220,7 @@ class StreamingService:
|
|
221
220
|
def __init__(
|
222
221
|
self,
|
223
222
|
agentex_client: AsyncAgentex,
|
224
|
-
stream_repository:
|
223
|
+
stream_repository: StreamRepository,
|
225
224
|
):
|
226
225
|
self._agentex_client = agentex_client
|
227
226
|
self._stream_repository = stream_repository
|
@@ -2,7 +2,7 @@ from scale_gp import SGPClient, SGPClientError
|
|
2
2
|
|
3
3
|
from agentex import AsyncAgentex
|
4
4
|
from agentex.lib.core.adapters.llm.adapter_litellm import LiteLLMGateway
|
5
|
-
from agentex.lib.core.adapters.streams.adapter_redis import
|
5
|
+
from agentex.lib.core.adapters.streams.adapter_redis import RedisStreamRepository
|
6
6
|
from agentex.lib.core.services.adk.acp.acp import ACPService
|
7
7
|
from agentex.lib.core.services.adk.agent_task_tracker import AgentTaskTrackerService
|
8
8
|
from agentex.lib.core.services.adk.events import EventsService
|
@@ -57,7 +57,7 @@ def get_all_activities(sgp_client=None):
|
|
57
57
|
sgp_client = None
|
58
58
|
|
59
59
|
llm_gateway = LiteLLMGateway()
|
60
|
-
stream_repository =
|
60
|
+
stream_repository = RedisStreamRepository()
|
61
61
|
agentex_client = AsyncAgentex()
|
62
62
|
tracer = AsyncTracer(agentex_client)
|
63
63
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: agentex-sdk
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: The official Python library for the agentex API
|
5
5
|
Project-URL: Homepage, https://github.com/scaleapi/agentex-python
|
6
6
|
Project-URL: Repository, https://github.com/scaleapi/agentex-python
|