glaip-sdk 0.0.3__py3-none-any.whl → 0.0.5__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 (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +146 -0
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -5,11 +5,14 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ import io
8
9
  import json
9
10
  import logging
11
+ from collections.abc import AsyncGenerator
10
12
  from time import monotonic
11
13
  from typing import Any, BinaryIO
12
14
 
15
+ import httpx
13
16
  from rich.console import Console as _Console
14
17
 
15
18
  from glaip_sdk.client.base import BaseClient
@@ -21,8 +24,10 @@ from glaip_sdk.config.constants import (
21
24
  DEFAULT_AGENT_VERSION,
22
25
  DEFAULT_MODEL,
23
26
  )
27
+ from glaip_sdk.exceptions import NotFoundError
24
28
  from glaip_sdk.models import Agent
25
29
  from glaip_sdk.utils.client_utils import (
30
+ aiter_sse_events,
26
31
  create_model_instances,
27
32
  extract_ids,
28
33
  find_by_name,
@@ -31,6 +36,11 @@ from glaip_sdk.utils.client_utils import (
31
36
  )
32
37
  from glaip_sdk.utils.rendering.models import RunStats
33
38
  from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
39
+ from glaip_sdk.utils.rendering.renderer.config import RendererConfig
40
+ from glaip_sdk.utils.validation import validate_agent_instruction
41
+
42
+ # API endpoints
43
+ AGENTS_ENDPOINT = "/agents/"
34
44
 
35
45
  # Set up module-level logger
36
46
  logger = logging.getLogger("glaip_sdk.agents")
@@ -48,14 +58,87 @@ class AgentClient(BaseClient):
48
58
  """
49
59
  super().__init__(parent_client=parent_client, **kwargs)
50
60
 
51
- def list_agents(self) -> list[Agent]:
52
- """List all agents."""
53
- data = self._request("GET", "/agents/")
61
+ def list_agents(
62
+ self,
63
+ agent_type: str | None = None,
64
+ framework: str | None = None,
65
+ name: str | None = None,
66
+ version: str | None = None,
67
+ sync_langflow_agents: bool = False,
68
+ ) -> list[Agent]:
69
+ """List agents with optional filtering.
70
+
71
+ Args:
72
+ agent_type: Filter by agent type (config, code, a2a)
73
+ framework: Filter by framework (langchain, langgraph, google_adk)
74
+ name: Filter by partial name match (case-insensitive)
75
+ version: Filter by exact version match
76
+ sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
77
+
78
+ Returns:
79
+ List of agents matching the filters
80
+ """
81
+ params = {}
82
+ if agent_type is not None:
83
+ params["agent_type"] = agent_type
84
+ if framework is not None:
85
+ params["framework"] = framework
86
+ if name is not None:
87
+ params["name"] = name
88
+ if version is not None:
89
+ params["version"] = version
90
+ if sync_langflow_agents:
91
+ params["sync_langflow_agents"] = "true"
92
+
93
+ if params:
94
+ data = self._request("GET", AGENTS_ENDPOINT, params=params)
95
+ else:
96
+ data = self._request("GET", AGENTS_ENDPOINT)
54
97
  return create_model_instances(data, Agent, self)
55
98
 
99
+ def sync_langflow_agents(
100
+ self,
101
+ base_url: str | None = None,
102
+ api_key: str | None = None,
103
+ ) -> dict[str, Any]:
104
+ """Sync LangFlow agents by fetching flows from the LangFlow server.
105
+
106
+ This method synchronizes agents with LangFlow flows. It fetches all flows
107
+ from the configured LangFlow server and creates/updates corresponding agents.
108
+
109
+ Args:
110
+ base_url: Custom LangFlow server base URL. If not provided, uses LANGFLOW_BASE_URL env var.
111
+ api_key: Custom LangFlow API key. If not provided, uses LANGFLOW_API_KEY env var.
112
+
113
+ Returns:
114
+ Response containing sync results and statistics
115
+
116
+ Raises:
117
+ ValueError: If LangFlow server configuration is missing
118
+ """
119
+ payload = {}
120
+ if base_url is not None:
121
+ payload["base_url"] = base_url
122
+ if api_key is not None:
123
+ payload["api_key"] = api_key
124
+
125
+ return self._request("POST", "/agents/langflow/sync", json=payload)
126
+
56
127
  def get_agent_by_id(self, agent_id: str) -> Agent:
57
128
  """Get agent by ID."""
58
129
  data = self._request("GET", f"/agents/{agent_id}")
130
+
131
+ if isinstance(data, str):
132
+ # Some backends may respond with plain text for missing agents.
133
+ message = data.strip() or f"Agent '{agent_id}' not found"
134
+ raise NotFoundError(message, status_code=404)
135
+
136
+ if not isinstance(data, dict):
137
+ raise NotFoundError(
138
+ f"Agent '{agent_id}' not found (unexpected response type)",
139
+ status_code=404,
140
+ )
141
+
59
142
  return Agent(**data)._set_client(self)
60
143
 
61
144
  def find_agents(self, name: str | None = None) -> list[Agent]:
@@ -64,13 +147,13 @@ class AgentClient(BaseClient):
64
147
  if name:
65
148
  params["name"] = name
66
149
 
67
- data = self._request("GET", "/agents/", params=params)
150
+ data = self._request("GET", AGENTS_ENDPOINT, params=params)
68
151
  agents = create_model_instances(data, Agent, self)
69
152
  if name is None:
70
153
  return agents
71
154
  return find_by_name(agents, name, case_sensitive=False)
72
155
 
73
- def create_agent(
156
+ def _build_create_payload(
74
157
  self,
75
158
  name: str,
76
159
  instruction: str,
@@ -79,38 +162,53 @@ class AgentClient(BaseClient):
79
162
  agents: list[str | Any] | None = None,
80
163
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
81
164
  **kwargs,
82
- ) -> "Agent":
83
- """Create a new agent."""
84
- # Client-side validation
85
- if not name or not name.strip():
86
- raise ValueError("Agent name cannot be empty or whitespace")
87
-
88
- if not instruction or not instruction.strip():
89
- raise ValueError("Agent instruction cannot be empty or whitespace")
165
+ ) -> dict[str, Any]:
166
+ """Build payload for agent creation with proper LM selection and metadata handling.
90
167
 
91
- if len(instruction.strip()) < 10:
92
- raise ValueError("Agent instruction must be at least 10 characters long")
168
+ CENTRALIZED PAYLOAD BUILDING LOGIC:
169
+ - LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
170
+ - Always includes required backend metadata
171
+ - Preserves mem0 keys in agent_config
172
+ - Handles tool/agent ID extraction from objects
93
173
 
94
- # Prepare the creation payload
174
+ Args:
175
+ name: Agent name
176
+ instruction: Agent instruction
177
+ model: Language model name (used when language_model_id not provided)
178
+ tools: List of tools to attach
179
+ agents: List of sub-agents to attach
180
+ timeout: Agent execution timeout
181
+ **kwargs: Additional parameters (language_model_id, agent_config, etc.)
182
+
183
+ Returns:
184
+ Complete payload dictionary for agent creation
185
+ """
186
+ # Prepare the creation payload with required fields
95
187
  payload: dict[str, Any] = {
96
188
  "name": name.strip(),
97
189
  "instruction": instruction.strip(),
98
190
  "type": DEFAULT_AGENT_TYPE,
99
191
  "framework": DEFAULT_AGENT_FRAMEWORK,
100
192
  "version": DEFAULT_AGENT_VERSION,
101
- "provider": DEFAULT_AGENT_PROVIDER,
102
- "model_name": model or DEFAULT_MODEL, # Ensure model_name is never None
103
193
  }
104
194
 
105
- # Include default execution timeout if provided
195
+ # Language model selection with exclusivity:
196
+ # Priority: language_model_id (if provided) > provider/model_name (fallback)
197
+ if kwargs.get("language_model_id"):
198
+ # Use language_model_id - defer to kwargs update below
199
+ pass
200
+ else:
201
+ # Use provider/model_name fallback
202
+ payload["provider"] = DEFAULT_AGENT_PROVIDER
203
+ payload["model_name"] = model or DEFAULT_MODEL
204
+
205
+ # Include execution timeout if provided
106
206
  if timeout is not None:
107
207
  payload["timeout"] = str(timeout)
108
208
 
109
209
  # Ensure minimum required metadata for visibility
110
210
  if "metadata" not in kwargs:
111
211
  kwargs["metadata"] = {}
112
-
113
- # Always include the minimum required metadata for visibility
114
212
  if "type" not in kwargs["metadata"]:
115
213
  kwargs["metadata"]["type"] = "custom"
116
214
 
@@ -124,32 +222,16 @@ class AgentClient(BaseClient):
124
222
  if agent_ids:
125
223
  payload["agents"] = agent_ids
126
224
 
127
- # Add any additional kwargs
225
+ # Add any additional kwargs (including language_model_id, agent_config, etc.)
128
226
  payload.update(kwargs)
129
227
 
130
- # Create the agent and fetch full details
131
- full_agent_data = self._post_then_fetch(
132
- id_key="id",
133
- post_endpoint="/agents/",
134
- get_endpoint_fmt="/agents/{id}",
135
- json=payload,
136
- )
137
- return Agent(**full_agent_data)._set_client(self)
228
+ return payload
138
229
 
139
- def update_agent(
140
- self,
141
- agent_id: str,
142
- name: str | None = None,
143
- instruction: str | None = None,
144
- model: str | None = None,
145
- **kwargs,
146
- ) -> "Agent":
147
- """Update an existing agent."""
148
- # First, get the current agent data
149
- current_agent = self.get_agent_by_id(agent_id)
150
-
151
- # Prepare the update payload with current values as defaults
152
- update_data = {
230
+ def _build_basic_update_payload(
231
+ self, current_agent: "Agent", name: str | None, instruction: str | None
232
+ ) -> dict[str, Any]:
233
+ """Build the basic update payload with required fields."""
234
+ return {
153
235
  "name": name if name is not None else current_agent.name,
154
236
  "instruction": instruction
155
237
  if instruction is not None
@@ -159,48 +241,250 @@ class AgentClient(BaseClient):
159
241
  "version": DEFAULT_AGENT_VERSION, # Required by backend
160
242
  }
161
243
 
162
- # Handle model specification
163
- if model is not None:
164
- update_data["provider"] = DEFAULT_AGENT_PROVIDER # Default provider
244
+ def _handle_language_model_selection(
245
+ self,
246
+ update_data: dict[str, Any],
247
+ current_agent: "Agent",
248
+ model: str | None,
249
+ language_model_id: str | None,
250
+ ) -> None:
251
+ """Handle language model selection with proper priority and fallbacks."""
252
+ if language_model_id:
253
+ # Use language_model_id if provided
254
+ update_data["language_model_id"] = language_model_id
255
+ elif model is not None:
256
+ # Use explicit model parameter
257
+ update_data["provider"] = DEFAULT_AGENT_PROVIDER
165
258
  update_data["model_name"] = model
166
259
  else:
167
- # Use current model if available
168
- if hasattr(current_agent, "agent_config") and current_agent.agent_config:
169
- if "lm_provider" in current_agent.agent_config:
170
- update_data["provider"] = current_agent.agent_config["lm_provider"]
171
- if "lm_name" in current_agent.agent_config:
172
- update_data["model_name"] = current_agent.agent_config["lm_name"]
173
- else:
174
- # Default values
175
- update_data["provider"] = DEFAULT_AGENT_PROVIDER
176
- update_data["model_name"] = DEFAULT_MODEL
260
+ # Use current agent config or fallbacks
261
+ self._set_language_model_from_current_agent(update_data, current_agent)
262
+
263
+ def _set_language_model_from_current_agent(
264
+ self, update_data: dict[str, Any], current_agent: "Agent"
265
+ ) -> None:
266
+ """Set language model from current agent config or use defaults."""
267
+ if hasattr(current_agent, "agent_config") and current_agent.agent_config:
268
+ agent_config = current_agent.agent_config
269
+ if "lm_provider" in agent_config:
270
+ update_data["provider"] = agent_config["lm_provider"]
271
+ if "lm_name" in agent_config:
272
+ update_data["model_name"] = agent_config["lm_name"]
273
+ else:
274
+ # Default fallback values
275
+ update_data["provider"] = DEFAULT_AGENT_PROVIDER
276
+ update_data["model_name"] = DEFAULT_MODEL
177
277
 
178
- # Handle tools and agents
179
- if "tools" in kwargs:
180
- tool_ids = extract_ids(kwargs["tools"])
181
- if tool_ids:
182
- update_data["tools"] = tool_ids
183
- elif current_agent.tools:
184
- update_data["tools"] = [
278
+ def _handle_tools_and_agents(
279
+ self,
280
+ update_data: dict[str, Any],
281
+ current_agent: "Agent",
282
+ tools: list | None,
283
+ agents: list | None,
284
+ ) -> None:
285
+ """Handle tools and agents with proper ID extraction."""
286
+ # Handle tools
287
+ if tools is not None:
288
+ tool_ids = extract_ids(tools)
289
+ update_data["tools"] = tool_ids if tool_ids else []
290
+ else:
291
+ update_data["tools"] = self._extract_current_tool_ids(current_agent)
292
+
293
+ # Handle agents
294
+ if agents is not None:
295
+ agent_ids = extract_ids(agents)
296
+ update_data["agents"] = agent_ids if agent_ids else []
297
+ else:
298
+ update_data["agents"] = self._extract_current_agent_ids(current_agent)
299
+
300
+ def _extract_current_tool_ids(self, current_agent: "Agent") -> list[str]:
301
+ """Extract tool IDs from current agent."""
302
+ if current_agent.tools:
303
+ return [
185
304
  tool["id"] if isinstance(tool, dict) else tool
186
305
  for tool in current_agent.tools
187
306
  ]
307
+ return []
188
308
 
189
- if "agents" in kwargs:
190
- agent_ids = extract_ids(kwargs["agents"])
191
- if agent_ids:
192
- update_data["agents"] = agent_ids
193
- elif current_agent.agents:
194
- update_data["agents"] = [
309
+ def _extract_current_agent_ids(self, current_agent: "Agent") -> list[str]:
310
+ """Extract agent IDs from current agent."""
311
+ if current_agent.agents:
312
+ return [
195
313
  agent["id"] if isinstance(agent, dict) else agent
196
314
  for agent in current_agent.agents
197
315
  ]
316
+ return []
198
317
 
199
- # Add any other kwargs
318
+ def _handle_agent_config(
319
+ self,
320
+ update_data: dict[str, Any],
321
+ current_agent: "Agent",
322
+ agent_config: dict | None,
323
+ ) -> None:
324
+ """Handle agent_config with proper merging and cleanup."""
325
+ if agent_config is not None:
326
+ # Use provided agent_config, merging with current if needed
327
+ update_data["agent_config"] = self._merge_agent_configs(
328
+ current_agent, agent_config
329
+ )
330
+ elif hasattr(current_agent, "agent_config") and current_agent.agent_config:
331
+ # Preserve existing agent_config
332
+ update_data["agent_config"] = current_agent.agent_config.copy()
333
+ else:
334
+ # Default agent_config
335
+ update_data["agent_config"] = {
336
+ "lm_provider": DEFAULT_AGENT_PROVIDER,
337
+ "lm_name": DEFAULT_MODEL,
338
+ "lm_hyperparameters": {"temperature": 0.0},
339
+ }
340
+
341
+ # Clean LM keys from agent_config to prevent conflicts
342
+ self._clean_agent_config_lm_keys(update_data)
343
+
344
+ def _merge_agent_configs(self, current_agent: "Agent", new_config: dict) -> dict:
345
+ """Merge current agent config with new config."""
346
+ if hasattr(current_agent, "agent_config") and current_agent.agent_config:
347
+ merged_config = current_agent.agent_config.copy()
348
+ merged_config.update(new_config)
349
+ return merged_config
350
+ return new_config
351
+
352
+ def _clean_agent_config_lm_keys(self, update_data: dict[str, Any]) -> None:
353
+ """Remove LM keys from agent_config to prevent conflicts."""
354
+ if "agent_config" in update_data and isinstance(
355
+ update_data["agent_config"], dict
356
+ ):
357
+ agent_config = update_data["agent_config"]
358
+ lm_keys_to_remove = {
359
+ "lm_provider",
360
+ "lm_name",
361
+ "lm_base_url",
362
+ "lm_hyperparameters",
363
+ }
364
+ for key in lm_keys_to_remove:
365
+ agent_config.pop(key, None)
366
+
367
+ def _finalize_update_payload(
368
+ self, update_data: dict[str, Any], current_agent: "Agent", **kwargs
369
+ ) -> dict[str, Any]:
370
+ """Finalize the update payload with metadata and additional kwargs."""
371
+ # Handle metadata preservation
372
+ if hasattr(current_agent, "metadata") and current_agent.metadata:
373
+ update_data["metadata"] = current_agent.metadata.copy()
374
+
375
+ # Add any other kwargs (excluding already handled ones)
376
+ excluded_keys = {"tools", "agents", "agent_config", "language_model_id"}
200
377
  for key, value in kwargs.items():
201
- if key not in ["tools", "agents"]:
378
+ if key not in excluded_keys:
202
379
  update_data[key] = value
203
380
 
381
+ return update_data
382
+
383
+ def _build_update_payload(
384
+ self,
385
+ current_agent: "Agent",
386
+ name: str | None = None,
387
+ instruction: str | None = None,
388
+ model: str | None = None,
389
+ **kwargs,
390
+ ) -> dict[str, Any]:
391
+ """Build payload for agent update with proper LM selection and current state preservation.
392
+
393
+ Args:
394
+ current_agent: Current agent object to update
395
+ name: New agent name (None to keep current)
396
+ instruction: New instruction (None to keep current)
397
+ model: New language model name (None to use current or fallback)
398
+ **kwargs: Additional parameters including language_model_id, agent_config, etc.
399
+
400
+ Returns:
401
+ Complete payload dictionary for agent update
402
+
403
+ Notes:
404
+ - LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
405
+ - Preserves current values as defaults when new values not provided
406
+ - Handles tools/agents updates with proper ID extraction
407
+ """
408
+ # Build basic payload
409
+ update_data = self._build_basic_update_payload(current_agent, name, instruction)
410
+
411
+ # Handle language model selection
412
+ language_model_id = kwargs.get("language_model_id")
413
+ self._handle_language_model_selection(
414
+ update_data, current_agent, model, language_model_id
415
+ )
416
+
417
+ # Handle tools and agents
418
+ tools = kwargs.get("tools")
419
+ agents = kwargs.get("agents")
420
+ self._handle_tools_and_agents(update_data, current_agent, tools, agents)
421
+
422
+ # Handle agent config
423
+ agent_config = kwargs.get("agent_config")
424
+ self._handle_agent_config(update_data, current_agent, agent_config)
425
+
426
+ # Finalize payload
427
+ return self._finalize_update_payload(update_data, current_agent, **kwargs)
428
+
429
+ def create_agent(
430
+ self,
431
+ name: str,
432
+ instruction: str,
433
+ model: str = DEFAULT_MODEL,
434
+ tools: list[str | Any] | None = None,
435
+ agents: list[str | Any] | None = None,
436
+ timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
437
+ **kwargs,
438
+ ) -> "Agent":
439
+ """Create a new agent."""
440
+ # Client-side validation
441
+ if not name or not name.strip():
442
+ raise ValueError("Agent name cannot be empty or whitespace")
443
+
444
+ # Validate instruction using centralized validation
445
+ instruction = validate_agent_instruction(instruction)
446
+
447
+ # Build payload using centralized builder
448
+ payload = self._build_create_payload(
449
+ name=name,
450
+ instruction=instruction,
451
+ model=model,
452
+ tools=tools,
453
+ agents=agents,
454
+ timeout=timeout,
455
+ **kwargs,
456
+ )
457
+
458
+ # Create the agent and fetch full details
459
+ full_agent_data = self._post_then_fetch(
460
+ id_key="id",
461
+ post_endpoint=AGENTS_ENDPOINT,
462
+ get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
463
+ json=payload,
464
+ )
465
+ return Agent(**full_agent_data)._set_client(self)
466
+
467
+ def update_agent(
468
+ self,
469
+ agent_id: str,
470
+ name: str | None = None,
471
+ instruction: str | None = None,
472
+ model: str | None = None,
473
+ **kwargs,
474
+ ) -> "Agent":
475
+ """Update an existing agent."""
476
+ # First, get the current agent data
477
+ current_agent = self.get_agent_by_id(agent_id)
478
+
479
+ # Build payload using centralized builder
480
+ update_data = self._build_update_payload(
481
+ current_agent=current_agent,
482
+ name=name,
483
+ instruction=instruction,
484
+ model=model,
485
+ **kwargs,
486
+ )
487
+
204
488
  # Send the complete payload
205
489
  data = self._request("PUT", f"/agents/{agent_id}", json=update_data)
206
490
  return Agent(**data)._set_client(self)
@@ -209,20 +493,12 @@ class AgentClient(BaseClient):
209
493
  """Delete an agent."""
210
494
  self._request("DELETE", f"/agents/{agent_id}")
211
495
 
212
- def run_agent(
213
- self,
214
- agent_id: str,
215
- message: str,
216
- files: list[str | BinaryIO] | None = None,
217
- tty: bool = False,
218
- *,
219
- renderer: RichStreamRenderer | str | None = "auto",
220
- **kwargs,
221
- ) -> str:
222
- """Run an agent with a message, streaming via a renderer."""
223
- # Prepare multipart data if files are provided
496
+ def _prepare_payload_and_headers(
497
+ self, message: str, files: list[str | BinaryIO] | None, tty: bool, **kwargs
498
+ ):
499
+ """Prepare payload and headers for agent run request."""
224
500
  multipart_data = None
225
- headers = None # None means "don't override client defaults"
501
+ headers = None
226
502
 
227
503
  if files:
228
504
  multipart_data = prepare_multipart_data(message, files)
@@ -231,49 +507,195 @@ class AgentClient(BaseClient):
231
507
  multipart_data.data["chat_history"] = kwargs["chat_history"]
232
508
  if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
233
509
  multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
234
- headers = None # Let httpx set proper multipart boundaries
235
510
 
236
511
  # When streaming, explicitly prefer SSE
237
512
  headers = {**(headers or {}), "Accept": "text/event-stream"}
238
513
 
239
514
  if files:
240
515
  payload = None
241
- # Use multipart data
242
516
  data_payload = multipart_data.data
243
517
  files_payload = multipart_data.files
244
518
  else:
245
519
  payload = {"input": message, **kwargs}
246
520
  if tty:
247
521
  payload["tty"] = True
248
- # Explicitly send stream intent both ways
249
522
  payload["stream"] = True
250
523
  data_payload = None
251
524
  files_payload = None
252
525
 
253
- # Choose renderer: use provided instance or create a default
526
+ return payload, data_payload, files_payload, headers, multipart_data
527
+
528
+ def _create_renderer(self, renderer, **kwargs):
529
+ """Create appropriate renderer based on configuration."""
254
530
  if isinstance(renderer, RichStreamRenderer):
255
- r = renderer
531
+ return renderer
532
+
533
+ verbose = kwargs.get("verbose", False)
534
+
535
+ if isinstance(renderer, str):
536
+ if renderer == "silent":
537
+ return self._create_silent_renderer()
538
+ elif renderer == "minimal":
539
+ return self._create_minimal_renderer()
540
+ else:
541
+ return self._create_default_renderer(verbose)
542
+ elif verbose:
543
+ return self._create_verbose_renderer()
256
544
  else:
257
- # Default to a standard rich renderer
258
- r = RichStreamRenderer(console=_Console())
545
+ return self._create_default_renderer(verbose)
546
+
547
+ def _create_silent_renderer(self):
548
+ """Create a silent renderer that suppresses all output."""
549
+ silent_config = RendererConfig(
550
+ live=False,
551
+ persist_live=False,
552
+ show_delegate_tool_panels=False,
553
+ render_thinking=False,
554
+ )
555
+ return RichStreamRenderer(
556
+ console=_Console(file=io.StringIO(), force_terminal=False),
557
+ cfg=silent_config,
558
+ verbose=False,
559
+ )
259
560
 
260
- # Try to set some meta early; refine as we receive events
261
- meta = {
262
- "agent_name": kwargs.get("agent_name", agent_id),
263
- "model": kwargs.get("model"),
264
- "run_id": None,
265
- "input_message": message, # Add the original query for context
266
- }
267
- r.on_start(meta)
561
+ def _create_minimal_renderer(self):
562
+ """Create a minimal renderer with basic output."""
563
+ minimal_config = RendererConfig(
564
+ live=False,
565
+ persist_live=False,
566
+ show_delegate_tool_panels=False,
567
+ render_thinking=False,
568
+ )
569
+ return RichStreamRenderer(
570
+ console=_Console(),
571
+ cfg=minimal_config,
572
+ verbose=False,
573
+ )
574
+
575
+ def _create_verbose_renderer(self):
576
+ """Create a verbose renderer for detailed output."""
577
+ verbose_config = RendererConfig(
578
+ theme="dark",
579
+ style="debug",
580
+ live=False,
581
+ show_delegate_tool_panels=True,
582
+ append_finished_snapshots=False,
583
+ )
584
+ return RichStreamRenderer(
585
+ console=_Console(),
586
+ cfg=verbose_config,
587
+ verbose=True,
588
+ )
268
589
 
590
+ def _create_default_renderer(self, verbose: bool):
591
+ """Create the default renderer."""
592
+ if verbose:
593
+ return self._create_verbose_renderer()
594
+ else:
595
+ default_config = RendererConfig(show_delegate_tool_panels=True)
596
+ return RichStreamRenderer(console=_Console(), cfg=default_config)
597
+
598
+ def _process_stream_events(
599
+ self, stream_response, renderer, timeout_seconds, agent_name, kwargs
600
+ ):
601
+ """Process streaming events and accumulate response."""
269
602
  final_text = ""
270
603
  stats_usage = {}
271
604
  started_monotonic = None
272
605
  finished_monotonic = None
606
+ meta = {
607
+ "agent_name": kwargs.get("agent_name", ""),
608
+ "model": kwargs.get("model"),
609
+ "run_id": None,
610
+ "input_message": "", # Will be set from kwargs if available
611
+ }
273
612
 
274
- # MultipartData handles file cleanup automatically
613
+ # Capture request id if provided
614
+ req_id = stream_response.headers.get(
615
+ "x-request-id"
616
+ ) or stream_response.headers.get("x-run-id")
617
+ if req_id:
618
+ meta["run_id"] = req_id
619
+ renderer.on_start(meta)
620
+
621
+ for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
622
+ try:
623
+ ev = json.loads(event["data"])
624
+ except json.JSONDecodeError:
625
+ logger.debug("Non-JSON SSE fragment skipped")
626
+ continue
627
+
628
+ # Start timer at first meaningful event
629
+ if started_monotonic is None and (
630
+ "content" in ev or "status" in ev or ev.get("metadata")
631
+ ):
632
+ started_monotonic = monotonic()
633
+
634
+ kind = (ev.get("metadata") or {}).get("kind")
635
+ renderer.on_event(ev)
636
+
637
+ # Skip artifacts from content accumulation
638
+ if kind == "artifact":
639
+ continue
640
+
641
+ # Accumulate assistant content
642
+ if ev.get("content"):
643
+ if not ev["content"].startswith("Artifact received:"):
644
+ final_text = ev["content"]
645
+ continue
646
+
647
+ # Handle final response
648
+ if kind == "final_response" and ev.get("content"):
649
+ final_text = ev["content"]
650
+ continue
651
+
652
+ # Handle usage stats
653
+ if kind == "usage":
654
+ stats_usage.update(ev.get("usage") or {})
655
+ continue
656
+
657
+ # Handle run info updates
658
+ if kind == "run_info":
659
+ if ev.get("model"):
660
+ meta["model"] = ev["model"]
661
+ renderer.on_start(meta)
662
+ if ev.get("run_id"):
663
+ meta["run_id"] = ev["run_id"]
664
+ renderer.on_start(meta)
665
+
666
+ finished_monotonic = monotonic()
667
+ return final_text, stats_usage, started_monotonic, finished_monotonic
668
+
669
+ def run_agent(
670
+ self,
671
+ agent_id: str,
672
+ message: str,
673
+ files: list[str | BinaryIO] | None = None,
674
+ tty: bool = False,
675
+ *,
676
+ renderer: RichStreamRenderer | str | None = "auto",
677
+ **kwargs,
678
+ ) -> str:
679
+ """Run an agent with a message, streaming via a renderer."""
680
+ # Prepare request payload and headers
681
+ payload, data_payload, files_payload, headers, multipart_data = (
682
+ self._prepare_payload_and_headers(message, files, tty, **kwargs)
683
+ )
684
+
685
+ # Create renderer
686
+ r = self._create_renderer(renderer, **kwargs)
687
+
688
+ # Initialize renderer
689
+ meta = {
690
+ "agent_name": kwargs.get("agent_name", agent_id),
691
+ "model": kwargs.get("model"),
692
+ "run_id": None,
693
+ "input_message": message,
694
+ }
695
+ r.on_start(meta)
275
696
 
276
697
  try:
698
+ # Make streaming request
277
699
  response = self.http_client.stream(
278
700
  "POST",
279
701
  f"/agents/{agent_id}/run",
@@ -286,71 +708,16 @@ class AgentClient(BaseClient):
286
708
  with response as stream_response:
287
709
  stream_response.raise_for_status()
288
710
 
289
- # capture request id if provided
290
- req_id = stream_response.headers.get(
291
- "x-request-id"
292
- ) or stream_response.headers.get("x-run-id")
293
- if req_id:
294
- meta["run_id"] = req_id
295
- r.on_start(meta) # refresh header with run_id
296
-
297
- # Get agent run timeout for execution control
298
- # Prefer CLI-provided timeout, otherwise use default
711
+ # Process streaming events
299
712
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
300
-
301
713
  agent_name = kwargs.get("agent_name")
302
714
 
303
- for event in iter_sse_events(
304
- stream_response, timeout_seconds, agent_name
305
- ):
306
- try:
307
- ev = json.loads(event["data"])
308
- except json.JSONDecodeError:
309
- logger.debug("Non-JSON SSE fragment skipped")
310
- continue
311
-
312
- # Start timer at first meaningful event
313
- if started_monotonic is None and (
314
- "content" in ev or "status" in ev or ev.get("metadata")
315
- ):
316
- started_monotonic = monotonic()
317
-
318
- kind = (ev.get("metadata") or {}).get("kind")
319
-
320
- # Pass event to the renderer (always, don't filter)
321
- r.on_event(ev)
322
-
323
- # Hide "artifact" chatter from content accumulation only
324
- if kind == "artifact":
325
- continue
326
-
327
- # Accumulate assistant content, but do not print here
328
- if "content" in ev and ev["content"]:
329
- # Filter weird backend text like "Artifact received: ..."
330
- if not ev["content"].startswith("Artifact received:"):
331
- final_text = ev["content"] # replace with latest
332
- continue
333
-
334
- # Also treat final_response like content for CLI return value
335
- if kind == "final_response" and ev.get("content"):
336
- final_text = ev["content"] # ensure CLI non-empty
337
- continue
338
-
339
- # Usage/cost event (if your backend emits it)
340
- if kind == "usage":
341
- stats_usage.update(ev.get("usage") or {})
342
- continue
343
-
344
- # Model/run info (if emitted mid-stream)
345
- if kind == "run_info":
346
- if ev.get("model"):
347
- meta["model"] = ev["model"]
348
- r.on_start(meta)
349
- if ev.get("run_id"):
350
- meta["run_id"] = ev["run_id"]
351
- r.on_start(meta)
352
-
353
- finished_monotonic = monotonic()
715
+ final_text, stats_usage, started_monotonic, finished_monotonic = (
716
+ self._process_stream_events(
717
+ stream_response, r, timeout_seconds, agent_name, kwargs
718
+ )
719
+ )
720
+
354
721
  except KeyboardInterrupt:
355
722
  try:
356
723
  r.close()
@@ -362,26 +729,120 @@ class AgentClient(BaseClient):
362
729
  finally:
363
730
  raise
364
731
  finally:
365
- # Ensure we close any opened file handles from multipart
732
+ # Ensure cleanup
366
733
  if multipart_data:
367
734
  multipart_data.close()
368
735
 
369
- # Finalize stats
736
+ # Finalize and return result
370
737
  st = RunStats()
371
- # Ensure monotonic order (avoid negative -0.0s)
372
- if started_monotonic is None:
373
- started_monotonic = finished_monotonic
374
-
375
738
  st.started_at = started_monotonic or st.started_at
376
739
  st.finished_at = finished_monotonic or st.started_at
377
740
  st.usage = stats_usage
378
741
 
379
- # Prefer explicit content, otherwise fall back to what the renderer saw
742
+ # Get final content
380
743
  if hasattr(r, "state") and hasattr(r.state, "buffer"):
381
744
  rendered_text = "".join(r.state.buffer)
382
745
  else:
383
746
  rendered_text = ""
384
- final_payload = final_text or rendered_text or "No response content received."
385
747
 
748
+ final_payload = final_text or rendered_text or "No response content received."
386
749
  r.on_complete(st)
387
750
  return final_payload
751
+
752
+ async def arun_agent(
753
+ self,
754
+ agent_id: str,
755
+ message: str,
756
+ files: list[str | BinaryIO] | None = None,
757
+ *,
758
+ timeout: float | None = None,
759
+ **kwargs,
760
+ ) -> AsyncGenerator[dict, None]:
761
+ """Async run an agent with a message, yielding streaming JSON chunks.
762
+
763
+ Args:
764
+ agent_id: ID of the agent to run
765
+ message: Message to send to the agent
766
+ files: Optional list of files to include
767
+ timeout: Request timeout in seconds
768
+ **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
769
+
770
+ Yields:
771
+ Dictionary containing parsed JSON chunks from the streaming response
772
+
773
+ Raises:
774
+ AgentTimeoutError: When agent execution times out
775
+ httpx.TimeoutException: When general timeout occurs
776
+ Exception: For other unexpected errors
777
+ """
778
+ # Prepare multipart data if files are provided
779
+ multipart_data = None
780
+ headers = None # None means "don't override client defaults"
781
+
782
+ if files:
783
+ multipart_data = prepare_multipart_data(message, files)
784
+ # Inject optional multipart extras expected by backend
785
+ if "chat_history" in kwargs and kwargs["chat_history"] is not None:
786
+ multipart_data.data["chat_history"] = kwargs["chat_history"]
787
+ if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
788
+ multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
789
+ headers = None # Let httpx set proper multipart boundaries
790
+
791
+ # When streaming, explicitly prefer SSE
792
+ headers = {**(headers or {}), "Accept": "text/event-stream"}
793
+
794
+ if files:
795
+ payload = None
796
+ # Use multipart data
797
+ data_payload = multipart_data.data
798
+ files_payload = multipart_data.files
799
+ else:
800
+ payload = {"input": message, **kwargs}
801
+ # Explicitly send stream intent both ways
802
+ payload["stream"] = True
803
+ data_payload = None
804
+ files_payload = None
805
+
806
+ # Use timeout from parameter or instance default
807
+ request_timeout = timeout or self.timeout
808
+
809
+ try:
810
+ async_client_config = self._build_async_client(request_timeout)
811
+ if headers:
812
+ async_client_config["headers"] = {
813
+ **async_client_config["headers"],
814
+ **headers,
815
+ }
816
+
817
+ # Create async client for this request
818
+ async with httpx.AsyncClient(**async_client_config) as async_client:
819
+ async with async_client.stream(
820
+ "POST",
821
+ f"/agents/{agent_id}/run",
822
+ json=payload,
823
+ data=data_payload,
824
+ files=files_payload,
825
+ headers=headers,
826
+ ) as stream_response:
827
+ stream_response.raise_for_status()
828
+
829
+ # Get agent run timeout for execution control
830
+ # Prefer parameter timeout, otherwise use default
831
+ timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
832
+
833
+ agent_name = kwargs.get("agent_name")
834
+
835
+ async for event in aiter_sse_events(
836
+ stream_response, timeout_seconds, agent_name
837
+ ):
838
+ try:
839
+ chunk = json.loads(event["data"])
840
+ yield chunk
841
+ except json.JSONDecodeError:
842
+ logger.debug("Non-JSON SSE fragment skipped")
843
+ continue
844
+
845
+ finally:
846
+ # Ensure we close any opened file handles from multipart
847
+ if multipart_data:
848
+ multipart_data.close()