jaf-py 2.5.10__py3-none-any.whl ā 2.5.11__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.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +360 -279
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.10.dist-info ā jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.10.dist-info/RECORD +0 -96
- {jaf_py-2.5.10.dist-info ā jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.10.dist-info ā jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.10.dist-info ā jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.10.dist-info ā jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
jaf/a2a/agent_card.py
CHANGED
|
@@ -10,9 +10,7 @@ from .types import A2AAgent
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def generate_agent_card(
|
|
13
|
-
config: Dict[str, Any],
|
|
14
|
-
agents: Dict[str, A2AAgent],
|
|
15
|
-
base_url: str = "http://localhost:3000"
|
|
13
|
+
config: Dict[str, Any], agents: Dict[str, A2AAgent], base_url: str = "http://localhost:3000"
|
|
16
14
|
) -> Dict[str, Any]:
|
|
17
15
|
"""Pure function to generate Agent Card from A2A agents"""
|
|
18
16
|
return {
|
|
@@ -26,13 +24,13 @@ def generate_agent_card(
|
|
|
26
24
|
"capabilities": {
|
|
27
25
|
"streaming": True,
|
|
28
26
|
"pushNotifications": False,
|
|
29
|
-
"stateTransitionHistory": True
|
|
27
|
+
"stateTransitionHistory": True,
|
|
30
28
|
},
|
|
31
29
|
"defaultInputModes": ["text/plain", "application/json"],
|
|
32
30
|
"defaultOutputModes": ["text/plain", "application/json"],
|
|
33
31
|
"skills": generate_skills_from_agents(agents),
|
|
34
32
|
"securitySchemes": generate_security_schemes(),
|
|
35
|
-
"security": generate_security_requirements()
|
|
33
|
+
"security": generate_security_requirements(),
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
|
|
@@ -49,7 +47,7 @@ def generate_skills_from_agents(agents: Dict[str, A2AAgent]) -> List[Dict[str, A
|
|
|
49
47
|
"tags": ["general"] + [tool.name for tool in agent.tools],
|
|
50
48
|
"examples": generate_examples_for_agent(agent),
|
|
51
49
|
"inputModes": agent.supported_content_types,
|
|
52
|
-
"outputModes": agent.supported_content_types
|
|
50
|
+
"outputModes": agent.supported_content_types,
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
# Create individual skills for each tool
|
|
@@ -61,7 +59,7 @@ def generate_skills_from_agents(agents: Dict[str, A2AAgent]) -> List[Dict[str, A
|
|
|
61
59
|
"tags": [tool.name, "tool", agent_name],
|
|
62
60
|
"examples": generate_examples_for_tool(tool),
|
|
63
61
|
"inputModes": ["text/plain", "application/json"],
|
|
64
|
-
"outputModes": ["text/plain", "application/json"]
|
|
62
|
+
"outputModes": ["text/plain", "application/json"],
|
|
65
63
|
}
|
|
66
64
|
for tool in agent.tools
|
|
67
65
|
]
|
|
@@ -80,10 +78,7 @@ def generate_skills_from_agents(agents: Dict[str, A2AAgent]) -> List[Dict[str, A
|
|
|
80
78
|
|
|
81
79
|
def generate_examples_for_agent(agent: A2AAgent) -> List[str]:
|
|
82
80
|
"""Pure function to generate examples for an agent"""
|
|
83
|
-
base_examples = [
|
|
84
|
-
f"Ask {agent.name} for help",
|
|
85
|
-
f"What can {agent.name} do?"
|
|
86
|
-
]
|
|
81
|
+
base_examples = [f"Ask {agent.name} for help", f"What can {agent.name} do?"]
|
|
87
82
|
|
|
88
83
|
# Add tool-specific examples using functional approach
|
|
89
84
|
tool_examples = [
|
|
@@ -96,11 +91,7 @@ def generate_examples_for_agent(agent: A2AAgent) -> List[str]:
|
|
|
96
91
|
|
|
97
92
|
def generate_examples_for_tool(tool: Any) -> List[str]:
|
|
98
93
|
"""Pure function to generate examples for a tool"""
|
|
99
|
-
return [
|
|
100
|
-
f"Use {tool.name}",
|
|
101
|
-
tool.description,
|
|
102
|
-
f"Help me with {tool.name.replace('_', ' ')}"
|
|
103
|
-
]
|
|
94
|
+
return [f"Use {tool.name}", tool.description, f"Help me with {tool.name.replace('_', ' ')}"]
|
|
104
95
|
|
|
105
96
|
|
|
106
97
|
def generate_security_schemes() -> Dict[str, Any]:
|
|
@@ -109,14 +100,14 @@ def generate_security_schemes() -> Dict[str, Any]:
|
|
|
109
100
|
"bearerAuth": {
|
|
110
101
|
"type": "http",
|
|
111
102
|
"scheme": "bearer",
|
|
112
|
-
"description": "Bearer token authentication"
|
|
103
|
+
"description": "Bearer token authentication",
|
|
113
104
|
},
|
|
114
105
|
"apiKey": {
|
|
115
106
|
"type": "apiKey",
|
|
116
107
|
"in": "header",
|
|
117
108
|
"name": "X-API-Key",
|
|
118
|
-
"description": "API key authentication"
|
|
119
|
-
}
|
|
109
|
+
"description": "API key authentication",
|
|
110
|
+
},
|
|
120
111
|
}
|
|
121
112
|
|
|
122
113
|
|
|
@@ -128,7 +119,7 @@ def generate_security_requirements() -> List[Dict[str, List[str]]]:
|
|
|
128
119
|
# Optional bearer auth
|
|
129
120
|
{"bearerAuth": []},
|
|
130
121
|
# Optional API key
|
|
131
|
-
{"apiKey": []}
|
|
122
|
+
{"apiKey": []},
|
|
132
123
|
]
|
|
133
124
|
|
|
134
125
|
|
|
@@ -136,7 +127,7 @@ def generate_agent_card_for_agent(
|
|
|
136
127
|
agent_name: str,
|
|
137
128
|
agent: A2AAgent,
|
|
138
129
|
config: Optional[Dict[str, Any]] = None,
|
|
139
|
-
base_url: str = "http://localhost:3000"
|
|
130
|
+
base_url: str = "http://localhost:3000",
|
|
140
131
|
) -> Dict[str, Any]:
|
|
141
132
|
"""Pure function to generate Agent Card for a specific agent"""
|
|
142
133
|
agent_map = {agent_name: agent}
|
|
@@ -145,10 +136,10 @@ def generate_agent_card_for_agent(
|
|
|
145
136
|
"name": (config or {}).get("name", agent.name),
|
|
146
137
|
"description": (config or {}).get("description", agent.description),
|
|
147
138
|
"version": (config or {}).get("version", "1.0.0"),
|
|
148
|
-
"provider": (config or {}).get(
|
|
149
|
-
"
|
|
150
|
-
"url": "https://functional-agent-framework.com"
|
|
151
|
-
|
|
139
|
+
"provider": (config or {}).get(
|
|
140
|
+
"provider",
|
|
141
|
+
{"organization": "JAF Agent", "url": "https://functional-agent-framework.com"},
|
|
142
|
+
),
|
|
152
143
|
}
|
|
153
144
|
|
|
154
145
|
return generate_agent_card(card_config, agent_map, base_url)
|
|
@@ -159,55 +150,66 @@ def validate_agent_card(card: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
159
150
|
|
|
160
151
|
# Validate required fields functionally
|
|
161
152
|
required_field_errors = [
|
|
162
|
-
error
|
|
153
|
+
error
|
|
154
|
+
for field, error_msg in [
|
|
163
155
|
(card.get("name", "").strip(), "Agent card name is required"),
|
|
164
156
|
(card.get("description", "").strip(), "Agent card description is required"),
|
|
165
157
|
(card.get("url", "").strip(), "Agent card URL is required"),
|
|
166
158
|
(card.get("version", "").strip(), "Agent card version is required"),
|
|
167
|
-
(card.get("protocolVersion", "").strip(), "Protocol version is required")
|
|
168
|
-
]
|
|
159
|
+
(card.get("protocolVersion", "").strip(), "Protocol version is required"),
|
|
160
|
+
]
|
|
161
|
+
if not field
|
|
169
162
|
for error in [error_msg]
|
|
170
163
|
]
|
|
171
164
|
|
|
172
165
|
# Skills validation functionally
|
|
173
166
|
skills = card.get("skills", [])
|
|
174
167
|
skills_errors = (
|
|
175
|
-
["At least one skill is required"]
|
|
176
|
-
|
|
168
|
+
["At least one skill is required"]
|
|
169
|
+
if not skills
|
|
170
|
+
else [
|
|
177
171
|
error
|
|
178
172
|
for index, skill in enumerate(skills)
|
|
179
173
|
for error in [
|
|
180
174
|
f"Skill {index}: ID is required" if not skill.get("id", "").strip() else None,
|
|
181
175
|
f"Skill {index}: Name is required" if not skill.get("name", "").strip() else None,
|
|
182
|
-
f"Skill {index}: Description is required"
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
f"Skill {index}: Description is required"
|
|
177
|
+
if not skill.get("description", "").strip()
|
|
178
|
+
else None,
|
|
179
|
+
f"Skill {index}: At least one tag is required"
|
|
180
|
+
if not skill.get("tags") or len(skill.get("tags", [])) == 0
|
|
181
|
+
else None,
|
|
182
|
+
]
|
|
183
|
+
if error is not None
|
|
185
184
|
]
|
|
186
185
|
)
|
|
187
186
|
|
|
188
187
|
# Input/Output modes validation functionally
|
|
189
188
|
mode_errors = [
|
|
190
|
-
error
|
|
191
|
-
|
|
192
|
-
(
|
|
193
|
-
|
|
189
|
+
error
|
|
190
|
+
for condition, error in [
|
|
191
|
+
(
|
|
192
|
+
not card.get("defaultInputModes") or len(card.get("defaultInputModes", [])) == 0,
|
|
193
|
+
"At least one default input mode is required",
|
|
194
|
+
),
|
|
195
|
+
(
|
|
196
|
+
not card.get("defaultOutputModes") or len(card.get("defaultOutputModes", [])) == 0,
|
|
197
|
+
"At least one default output mode is required",
|
|
198
|
+
),
|
|
199
|
+
]
|
|
200
|
+
if condition
|
|
194
201
|
for error in [error]
|
|
195
202
|
]
|
|
196
203
|
|
|
197
204
|
# URL validation functionally
|
|
198
205
|
url_errors = [
|
|
199
|
-
"Invalid URL format"
|
|
200
|
-
for url in [card.get("url")]
|
|
201
|
-
if url and not is_valid_url(url)
|
|
206
|
+
"Invalid URL format" for url in [card.get("url")] if url and not is_valid_url(url)
|
|
202
207
|
]
|
|
203
208
|
|
|
204
209
|
# Combine all errors functionally
|
|
205
210
|
errors = required_field_errors + skills_errors + mode_errors + url_errors
|
|
206
211
|
|
|
207
|
-
return {
|
|
208
|
-
"is_valid": len(errors) == 0,
|
|
209
|
-
"errors": errors
|
|
210
|
-
}
|
|
212
|
+
return {"is_valid": len(errors) == 0, "errors": errors}
|
|
211
213
|
|
|
212
214
|
|
|
213
215
|
def is_valid_url(url: str) -> bool:
|
|
@@ -220,9 +222,7 @@ def is_valid_url(url: str) -> bool:
|
|
|
220
222
|
|
|
221
223
|
|
|
222
224
|
def create_minimal_agent_card(
|
|
223
|
-
name: str,
|
|
224
|
-
description: str,
|
|
225
|
-
url: str = "http://localhost:3000/a2a"
|
|
225
|
+
name: str, description: str, url: str = "http://localhost:3000/a2a"
|
|
226
226
|
) -> Dict[str, Any]:
|
|
227
227
|
"""Pure function to create minimal Agent Card"""
|
|
228
228
|
return {
|
|
@@ -235,17 +235,19 @@ def create_minimal_agent_card(
|
|
|
235
235
|
"capabilities": {
|
|
236
236
|
"streaming": True,
|
|
237
237
|
"pushNotifications": False,
|
|
238
|
-
"stateTransitionHistory": False
|
|
238
|
+
"stateTransitionHistory": False,
|
|
239
239
|
},
|
|
240
240
|
"defaultInputModes": ["text/plain"],
|
|
241
241
|
"defaultOutputModes": ["text/plain"],
|
|
242
|
-
"skills": [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
242
|
+
"skills": [
|
|
243
|
+
{
|
|
244
|
+
"id": "general",
|
|
245
|
+
"name": "General Assistant",
|
|
246
|
+
"description": "General purpose assistance",
|
|
247
|
+
"tags": ["general", "assistant"],
|
|
248
|
+
"examples": ["How can I help you?"],
|
|
249
|
+
}
|
|
250
|
+
],
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
|
|
@@ -258,11 +260,7 @@ def merge_agent_cards(*cards: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
258
260
|
additional_cards = cards[1:]
|
|
259
261
|
|
|
260
262
|
# Merge skills functionally
|
|
261
|
-
all_skills = [
|
|
262
|
-
skill
|
|
263
|
-
for card in cards
|
|
264
|
-
for skill in card.get("skills", [])
|
|
265
|
-
]
|
|
263
|
+
all_skills = [skill for card in cards for skill in card.get("skills", [])]
|
|
266
264
|
|
|
267
265
|
# Remove duplicate skills by ID functionally
|
|
268
266
|
seen_ids = set()
|
|
@@ -273,24 +271,35 @@ def merge_agent_cards(*cards: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
273
271
|
]
|
|
274
272
|
|
|
275
273
|
# Merge input/output modes
|
|
276
|
-
merged_input_modes = list(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
274
|
+
merged_input_modes = list(
|
|
275
|
+
set(
|
|
276
|
+
[
|
|
277
|
+
*base_card.get("defaultInputModes", []),
|
|
278
|
+
*[mode for card in additional_cards for mode in card.get("defaultInputModes", [])],
|
|
279
|
+
]
|
|
280
|
+
)
|
|
281
|
+
)
|
|
280
282
|
|
|
281
|
-
merged_output_modes = list(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
merged_output_modes = list(
|
|
284
|
+
set(
|
|
285
|
+
[
|
|
286
|
+
*base_card.get("defaultOutputModes", []),
|
|
287
|
+
*[mode for card in additional_cards for mode in card.get("defaultOutputModes", [])],
|
|
288
|
+
]
|
|
289
|
+
)
|
|
290
|
+
)
|
|
285
291
|
|
|
286
292
|
# Merge capabilities
|
|
287
293
|
capabilities = base_card.get("capabilities", {})
|
|
288
294
|
for card in additional_cards:
|
|
289
295
|
card_capabilities = card.get("capabilities", {})
|
|
290
296
|
capabilities = {
|
|
291
|
-
"streaming": capabilities.get("streaming", False)
|
|
292
|
-
|
|
293
|
-
"
|
|
297
|
+
"streaming": capabilities.get("streaming", False)
|
|
298
|
+
or card_capabilities.get("streaming", False),
|
|
299
|
+
"pushNotifications": capabilities.get("pushNotifications", False)
|
|
300
|
+
or card_capabilities.get("pushNotifications", False),
|
|
301
|
+
"stateTransitionHistory": capabilities.get("stateTransitionHistory", False)
|
|
302
|
+
or card_capabilities.get("stateTransitionHistory", False),
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
return {
|
|
@@ -298,7 +307,7 @@ def merge_agent_cards(*cards: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
298
307
|
"skills": unique_skills,
|
|
299
308
|
"defaultInputModes": merged_input_modes,
|
|
300
309
|
"defaultOutputModes": merged_output_modes,
|
|
301
|
-
"capabilities": capabilities
|
|
310
|
+
"capabilities": capabilities,
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
|
|
@@ -313,17 +322,17 @@ def create_agent_card_from_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
313
322
|
"url": f"{base_url}/a2a",
|
|
314
323
|
"preferredTransport": "JSONRPC",
|
|
315
324
|
"version": config.get("version", "1.0.0"),
|
|
316
|
-
"provider": config.get(
|
|
317
|
-
"
|
|
318
|
-
"url": "https://functional-agent-framework.com"
|
|
319
|
-
|
|
325
|
+
"provider": config.get(
|
|
326
|
+
"provider",
|
|
327
|
+
{"organization": "JAF Framework", "url": "https://functional-agent-framework.com"},
|
|
328
|
+
),
|
|
320
329
|
"capabilities": {
|
|
321
330
|
"streaming": True,
|
|
322
331
|
"pushNotifications": False,
|
|
323
332
|
"stateTransitionHistory": True,
|
|
324
|
-
**config.get("capabilities", {})
|
|
333
|
+
**config.get("capabilities", {}),
|
|
325
334
|
},
|
|
326
335
|
"defaultInputModes": ["text/plain", "application/json"],
|
|
327
336
|
"defaultOutputModes": ["text/plain", "application/json"],
|
|
328
|
-
"skills": generate_skills_from_agents(config.get("agents", {}))
|
|
337
|
+
"skills": generate_skills_from_agents(config.get("agents", {})),
|
|
329
338
|
}
|
jaf/a2a/client.py
CHANGED
|
@@ -24,16 +24,14 @@ def create_a2a_client(base_url: str, config: Optional[Dict[str, Any]] = None) ->
|
|
|
24
24
|
return A2AClientState(
|
|
25
25
|
config=A2AClientConfig(
|
|
26
26
|
baseUrl=base_url.rstrip("/"), # Remove trailing slash
|
|
27
|
-
timeout=config.get("timeout", 30000)
|
|
27
|
+
timeout=config.get("timeout", 30000),
|
|
28
28
|
),
|
|
29
|
-
sessionId=f"client_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|
29
|
+
sessionId=f"client_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}",
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def create_message_request(
|
|
34
|
-
message: str,
|
|
35
|
-
session_id: str,
|
|
36
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
34
|
+
message: str, session_id: str, configuration: Optional[Dict[str, Any]] = None
|
|
37
35
|
) -> Dict[str, Any]:
|
|
38
36
|
"""Pure function to create message request"""
|
|
39
37
|
return {
|
|
@@ -46,17 +44,15 @@ def create_message_request(
|
|
|
46
44
|
"parts": [{"kind": "text", "text": message}],
|
|
47
45
|
"messageId": f"msg_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}",
|
|
48
46
|
"contextId": session_id,
|
|
49
|
-
"kind": "message"
|
|
47
|
+
"kind": "message",
|
|
50
48
|
},
|
|
51
|
-
"configuration": configuration
|
|
52
|
-
}
|
|
49
|
+
"configuration": configuration,
|
|
50
|
+
},
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
|
|
56
54
|
def create_streaming_message_request(
|
|
57
|
-
message: str,
|
|
58
|
-
session_id: str,
|
|
59
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
55
|
+
message: str, session_id: str, configuration: Optional[Dict[str, Any]] = None
|
|
60
56
|
) -> Dict[str, Any]:
|
|
61
57
|
"""Pure function to create streaming message request"""
|
|
62
58
|
return {
|
|
@@ -69,18 +65,14 @@ def create_streaming_message_request(
|
|
|
69
65
|
"parts": [{"kind": "text", "text": message}],
|
|
70
66
|
"messageId": f"msg_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}",
|
|
71
67
|
"contextId": session_id,
|
|
72
|
-
"kind": "message"
|
|
68
|
+
"kind": "message",
|
|
73
69
|
},
|
|
74
|
-
"configuration": configuration
|
|
75
|
-
}
|
|
70
|
+
"configuration": configuration,
|
|
71
|
+
},
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
|
|
79
|
-
async def send_http_request(
|
|
80
|
-
url: str,
|
|
81
|
-
body: Dict[str, Any],
|
|
82
|
-
timeout: int = 30000
|
|
83
|
-
) -> Dict[str, Any]:
|
|
75
|
+
async def send_http_request(url: str, body: Dict[str, Any], timeout: int = 30000) -> Dict[str, Any]:
|
|
84
76
|
"""Pure function to send HTTP request"""
|
|
85
77
|
timeout_seconds = timeout / 1000.0
|
|
86
78
|
|
|
@@ -89,10 +81,7 @@ async def send_http_request(
|
|
|
89
81
|
response = await client.post(
|
|
90
82
|
url,
|
|
91
83
|
json=body,
|
|
92
|
-
headers={
|
|
93
|
-
"Content-Type": "application/json",
|
|
94
|
-
"Accept": "application/json"
|
|
95
|
-
}
|
|
84
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
96
85
|
)
|
|
97
86
|
|
|
98
87
|
if not response.is_success:
|
|
@@ -104,19 +93,14 @@ async def send_http_request(
|
|
|
104
93
|
raise Exception(f"Request timeout after {timeout}ms")
|
|
105
94
|
|
|
106
95
|
|
|
107
|
-
async def send_a2a_request(
|
|
108
|
-
client: A2AClientState,
|
|
109
|
-
request: Dict[str, Any]
|
|
110
|
-
) -> Dict[str, Any]:
|
|
96
|
+
async def send_a2a_request(client: A2AClientState, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
111
97
|
"""Pure function to send A2A request"""
|
|
112
98
|
url = f"{client.config.base_url}/a2a"
|
|
113
99
|
return await send_http_request(url, request, client.config.timeout)
|
|
114
100
|
|
|
115
101
|
|
|
116
102
|
async def send_message(
|
|
117
|
-
client: A2AClientState,
|
|
118
|
-
message: str,
|
|
119
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
103
|
+
client: A2AClientState, message: str, configuration: Optional[Dict[str, Any]] = None
|
|
120
104
|
) -> str:
|
|
121
105
|
"""Pure function to send message"""
|
|
122
106
|
request = create_message_request(message, client.session_id, configuration)
|
|
@@ -130,9 +114,7 @@ async def send_message(
|
|
|
130
114
|
|
|
131
115
|
|
|
132
116
|
async def stream_message(
|
|
133
|
-
client: A2AClientState,
|
|
134
|
-
message: str,
|
|
135
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
117
|
+
client: A2AClientState, message: str, configuration: Optional[Dict[str, Any]] = None
|
|
136
118
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
137
119
|
"""Pure function to stream message"""
|
|
138
120
|
request = create_streaming_message_request(message, client.session_id, configuration)
|
|
@@ -146,12 +128,8 @@ async def stream_message(
|
|
|
146
128
|
"POST",
|
|
147
129
|
url,
|
|
148
130
|
json=request,
|
|
149
|
-
headers={
|
|
150
|
-
"Content-Type": "application/json",
|
|
151
|
-
"Accept": "text/event-stream"
|
|
152
|
-
}
|
|
131
|
+
headers={"Content-Type": "application/json", "Accept": "text/event-stream"},
|
|
153
132
|
) as response:
|
|
154
|
-
|
|
155
133
|
if not response.is_success:
|
|
156
134
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
157
135
|
|
|
@@ -182,10 +160,7 @@ async def get_agent_card(client: A2AClientState) -> Dict[str, Any]:
|
|
|
182
160
|
url = f"{client.config.base_url}/.well-known/agent-card"
|
|
183
161
|
|
|
184
162
|
async with httpx.AsyncClient(timeout=client.config.timeout / 1000.0) as http_client:
|
|
185
|
-
response = await http_client.get(
|
|
186
|
-
url,
|
|
187
|
-
headers={"Accept": "application/json"}
|
|
188
|
-
)
|
|
163
|
+
response = await http_client.get(url, headers={"Accept": "application/json"})
|
|
189
164
|
|
|
190
165
|
if not response.is_success:
|
|
191
166
|
raise Exception(f"Failed to get agent card: HTTP {response.status_code}")
|
|
@@ -203,7 +178,7 @@ async def send_message_to_agent(
|
|
|
203
178
|
client: A2AClientState,
|
|
204
179
|
agent_name: str,
|
|
205
180
|
message: str,
|
|
206
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
181
|
+
configuration: Optional[Dict[str, Any]] = None,
|
|
207
182
|
) -> str:
|
|
208
183
|
"""Pure function to send message to specific agent"""
|
|
209
184
|
request = create_message_request(message, client.session_id, configuration)
|
|
@@ -222,7 +197,7 @@ async def stream_message_to_agent(
|
|
|
222
197
|
client: A2AClientState,
|
|
223
198
|
agent_name: str,
|
|
224
199
|
message: str,
|
|
225
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
200
|
+
configuration: Optional[Dict[str, Any]] = None,
|
|
226
201
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
227
202
|
"""Pure function to stream message to specific agent"""
|
|
228
203
|
request = create_streaming_message_request(message, client.session_id, configuration)
|
|
@@ -236,12 +211,8 @@ async def stream_message_to_agent(
|
|
|
236
211
|
"POST",
|
|
237
212
|
url,
|
|
238
213
|
json=request,
|
|
239
|
-
headers={
|
|
240
|
-
"Content-Type": "application/json",
|
|
241
|
-
"Accept": "text/event-stream"
|
|
242
|
-
}
|
|
214
|
+
headers={"Content-Type": "application/json", "Accept": "text/event-stream"},
|
|
243
215
|
) as response:
|
|
244
|
-
|
|
245
216
|
if not response.is_success:
|
|
246
217
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
247
218
|
|
|
@@ -320,10 +291,7 @@ async def check_a2a_health(client: A2AClientState) -> Dict[str, Any]:
|
|
|
320
291
|
url = f"{client.config.base_url}/a2a/health"
|
|
321
292
|
|
|
322
293
|
async with httpx.AsyncClient(timeout=client.config.timeout / 1000.0) as http_client:
|
|
323
|
-
response = await http_client.get(
|
|
324
|
-
url,
|
|
325
|
-
headers={"Accept": "application/json"}
|
|
326
|
-
)
|
|
294
|
+
response = await http_client.get(url, headers={"Accept": "application/json"})
|
|
327
295
|
|
|
328
296
|
if not response.is_success:
|
|
329
297
|
raise Exception(f"Health check failed: HTTP {response.status_code}")
|
|
@@ -336,10 +304,7 @@ async def get_a2a_capabilities(client: A2AClientState) -> Dict[str, Any]:
|
|
|
336
304
|
url = f"{client.config.base_url}/a2a/capabilities"
|
|
337
305
|
|
|
338
306
|
async with httpx.AsyncClient(timeout=client.config.timeout / 1000.0) as http_client:
|
|
339
|
-
response = await http_client.get(
|
|
340
|
-
url,
|
|
341
|
-
headers={"Accept": "application/json"}
|
|
342
|
-
)
|
|
307
|
+
response = await http_client.get(url, headers={"Accept": "application/json"})
|
|
343
308
|
|
|
344
309
|
if not response.is_success:
|
|
345
310
|
raise Exception(f"Capabilities request failed: HTTP {response.status_code}")
|
|
@@ -371,16 +336,15 @@ async def connect_to_a2a_agent(base_url: str) -> Dict[str, Any]:
|
|
|
371
336
|
"ask": ask,
|
|
372
337
|
"stream": stream,
|
|
373
338
|
"health": health,
|
|
374
|
-
"capabilities": capabilities
|
|
339
|
+
"capabilities": capabilities,
|
|
375
340
|
}
|
|
376
341
|
|
|
377
342
|
|
|
378
343
|
# Utility functions
|
|
379
344
|
|
|
345
|
+
|
|
380
346
|
def create_a2a_message_dict(
|
|
381
|
-
text: str,
|
|
382
|
-
role: str = "user",
|
|
383
|
-
context_id: Optional[str] = None
|
|
347
|
+
text: str, role: str = "user", context_id: Optional[str] = None
|
|
384
348
|
) -> Dict[str, Any]:
|
|
385
349
|
"""Utility function to create A2A message dictionary"""
|
|
386
350
|
return {
|
|
@@ -388,7 +352,7 @@ def create_a2a_message_dict(
|
|
|
388
352
|
"parts": [{"kind": "text", "text": text}],
|
|
389
353
|
"messageId": f"msg_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}",
|
|
390
354
|
"contextId": context_id,
|
|
391
|
-
"kind": "message"
|
|
355
|
+
"kind": "message",
|
|
392
356
|
}
|
|
393
357
|
|
|
394
358
|
|
|
@@ -410,8 +374,8 @@ def parse_sse_event(line: str) -> Optional[Dict[str, Any]]:
|
|
|
410
374
|
def validate_a2a_response(response: Dict[str, Any]) -> bool:
|
|
411
375
|
"""Pure function to validate A2A response"""
|
|
412
376
|
return (
|
|
413
|
-
isinstance(response, dict)
|
|
414
|
-
response.get("jsonrpc") == "2.0"
|
|
415
|
-
"id" in response
|
|
416
|
-
("result" in response or "error" in response)
|
|
377
|
+
isinstance(response, dict)
|
|
378
|
+
and response.get("jsonrpc") == "2.0"
|
|
379
|
+
and "id" in response
|
|
380
|
+
and ("result" in response or "error" in response)
|
|
417
381
|
)
|
|
@@ -7,13 +7,13 @@ using the JAF A2A client library.
|
|
|
7
7
|
Usage:
|
|
8
8
|
# Start the server first:
|
|
9
9
|
python server_example.py
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
# Then run this client:
|
|
12
12
|
python client_example.py
|
|
13
13
|
|
|
14
14
|
The client will connect to http://localhost:3000 and demonstrate:
|
|
15
15
|
- Agent discovery via Agent Cards
|
|
16
|
-
- Sending messages to specific agents
|
|
16
|
+
- Sending messages to specific agents
|
|
17
17
|
- Streaming message responses
|
|
18
18
|
- Health checks and capabilities
|
|
19
19
|
- Error handling
|
|
@@ -46,11 +46,11 @@ async def discover_server_agents(base_url: str):
|
|
|
46
46
|
print(f"š URL: {agent_card['url']}")
|
|
47
47
|
print(f"š” Protocol: {agent_card['protocolVersion']}")
|
|
48
48
|
|
|
49
|
-
skills = agent_card.get(
|
|
49
|
+
skills = agent_card.get("skills", [])
|
|
50
50
|
print(f"\nš ļø Available skills ({len(skills)}):")
|
|
51
51
|
for skill in skills:
|
|
52
52
|
print(f" ⢠{skill['name']}: {skill['description']}")
|
|
53
|
-
if skill.get(
|
|
53
|
+
if skill.get("tags"):
|
|
54
54
|
print(f" Tags: {', '.join(skill['tags'])}")
|
|
55
55
|
|
|
56
56
|
return agent_card
|
|
@@ -70,18 +70,18 @@ async def check_server_health(client):
|
|
|
70
70
|
print(f"š” Protocol: {health.get('protocol', 'unknown')}")
|
|
71
71
|
print(f"š¢ Version: {health.get('version', 'unknown')}")
|
|
72
72
|
|
|
73
|
-
agents = health.get(
|
|
73
|
+
agents = health.get("agents", [])
|
|
74
74
|
if agents:
|
|
75
75
|
print(f"š¤ Available agents: {', '.join(agents)}")
|
|
76
76
|
|
|
77
77
|
print("\nā” Getting capabilities...")
|
|
78
78
|
capabilities = await get_a2a_capabilities(client)
|
|
79
79
|
|
|
80
|
-
methods = capabilities.get(
|
|
80
|
+
methods = capabilities.get("supportedMethods", [])
|
|
81
81
|
if methods:
|
|
82
82
|
print(f"š Supported methods: {', '.join(methods)}")
|
|
83
83
|
|
|
84
|
-
transports = capabilities.get(
|
|
84
|
+
transports = capabilities.get("supportedTransports", [])
|
|
85
85
|
if transports:
|
|
86
86
|
print(f"š Supported transports: {', '.join(transports)}")
|
|
87
87
|
|
|
@@ -100,7 +100,7 @@ async def test_math_agent(client):
|
|
|
100
100
|
"What is 25 + 17?",
|
|
101
101
|
"Calculate 144 / 12",
|
|
102
102
|
"What is 2^8?",
|
|
103
|
-
"Can you solve (15 + 5) * 3?"
|
|
103
|
+
"Can you solve (15 + 5) * 3?",
|
|
104
104
|
]
|
|
105
105
|
|
|
106
106
|
for question in test_cases:
|
|
@@ -121,7 +121,7 @@ async def test_weather_agent(client):
|
|
|
121
121
|
"What's the weather in London?",
|
|
122
122
|
"How's the weather in Tokyo?",
|
|
123
123
|
"Tell me about the weather in New York",
|
|
124
|
-
"Weather forecast for Sydney please"
|
|
124
|
+
"Weather forecast for Sydney please",
|
|
125
125
|
]
|
|
126
126
|
|
|
127
127
|
for question in locations:
|
|
@@ -142,7 +142,7 @@ async def test_translator_agent(client):
|
|
|
142
142
|
"Translate 'Hello, how are you?' to Spanish",
|
|
143
143
|
"Can you translate 'Good morning' to French?",
|
|
144
144
|
"Translate 'Thank you very much' to German",
|
|
145
|
-
"Convert 'Beautiful day' to Italian"
|
|
145
|
+
"Convert 'Beautiful day' to Italian",
|
|
146
146
|
]
|
|
147
147
|
|
|
148
148
|
for question in translations:
|
|
@@ -179,7 +179,7 @@ async def test_general_assistant(client):
|
|
|
179
179
|
"Hello, what can you help me with?",
|
|
180
180
|
"Tell me a joke",
|
|
181
181
|
"What is the meaning of life?",
|
|
182
|
-
"How can I be more productive?"
|
|
182
|
+
"How can I be more productive?",
|
|
183
183
|
]
|
|
184
184
|
|
|
185
185
|
for question in questions:
|
|
@@ -212,7 +212,7 @@ async def test_convenience_connection():
|
|
|
212
212
|
|
|
213
213
|
# Test capabilities
|
|
214
214
|
capabilities = await connection["capabilities"]()
|
|
215
|
-
methods = capabilities.get(
|
|
215
|
+
methods = capabilities.get("supportedMethods", [])
|
|
216
216
|
print(f"ā” Methods via convenience: {len(methods)} methods")
|
|
217
217
|
|
|
218
218
|
return True
|