glaip-sdk 0.0.2__py3-none-any.whl → 0.0.3__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 (39) hide show
  1. glaip_sdk/__init__.py +2 -2
  2. glaip_sdk/_version.py +51 -0
  3. glaip_sdk/cli/commands/agents.py +201 -109
  4. glaip_sdk/cli/commands/configure.py +29 -87
  5. glaip_sdk/cli/commands/init.py +16 -7
  6. glaip_sdk/cli/commands/mcps.py +73 -153
  7. glaip_sdk/cli/commands/tools.py +185 -49
  8. glaip_sdk/cli/main.py +30 -27
  9. glaip_sdk/cli/utils.py +126 -13
  10. glaip_sdk/client/__init__.py +54 -2
  11. glaip_sdk/client/agents.py +175 -237
  12. glaip_sdk/client/base.py +62 -2
  13. glaip_sdk/client/mcps.py +63 -20
  14. glaip_sdk/client/tools.py +95 -28
  15. glaip_sdk/config/constants.py +10 -3
  16. glaip_sdk/exceptions.py +13 -0
  17. glaip_sdk/models.py +20 -4
  18. glaip_sdk/utils/__init__.py +116 -18
  19. glaip_sdk/utils/client_utils.py +284 -0
  20. glaip_sdk/utils/rendering/__init__.py +1 -0
  21. glaip_sdk/utils/rendering/formatting.py +211 -0
  22. glaip_sdk/utils/rendering/models.py +53 -0
  23. glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
  24. glaip_sdk/utils/rendering/renderer/base.py +827 -0
  25. glaip_sdk/utils/rendering/renderer/config.py +33 -0
  26. glaip_sdk/utils/rendering/renderer/console.py +54 -0
  27. glaip_sdk/utils/rendering/renderer/debug.py +82 -0
  28. glaip_sdk/utils/rendering/renderer/panels.py +123 -0
  29. glaip_sdk/utils/rendering/renderer/progress.py +118 -0
  30. glaip_sdk/utils/rendering/renderer/stream.py +198 -0
  31. glaip_sdk/utils/rendering/steps.py +168 -0
  32. glaip_sdk/utils/run_renderer.py +22 -1086
  33. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.3.dist-info}/METADATA +8 -36
  34. glaip_sdk-0.0.3.dist-info/RECORD +40 -0
  35. glaip_sdk/cli/config.py +0 -592
  36. glaip_sdk/utils.py +0 -167
  37. glaip_sdk-0.0.2.dist-info/RECORD +0 -28
  38. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.3.dist-info}/WHEEL +0 -0
  39. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,33 @@
1
+ """Configuration types for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class RendererConfig:
14
+ """Configuration for the RichStreamRenderer."""
15
+
16
+ # Style and layout
17
+ theme: str = "dark" # dark|light
18
+ style: str = "pretty" # pretty|debug|minimal
19
+
20
+ # Performance
21
+ think_threshold: float = 0.7
22
+ refresh_debounce: float = 0.25
23
+ render_thinking: bool = True
24
+ live: bool = True
25
+ persist_live: bool = True
26
+
27
+ # Debug visibility toggles
28
+ show_delegate_tool_panels: bool = False
29
+
30
+ # Scrollback/append options
31
+ append_finished_snapshots: bool = False
32
+ snapshot_max_chars: int = 4000
33
+ snapshot_max_lines: int = 60
@@ -0,0 +1,54 @@
1
+ """Console handling utilities for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+
11
+ from rich.console import Console as RichConsole
12
+
13
+
14
+ class CapturingConsole:
15
+ """Console wrapper that captures all output for saving."""
16
+
17
+ def __init__(self, original_console, capture=False):
18
+ """Initialize the capturing console.
19
+
20
+ Args:
21
+ original_console: The original Rich console instance
22
+ capture: Whether to capture output in addition to displaying it
23
+ """
24
+ self.original_console = original_console
25
+ self.capture = capture
26
+ self.captured_output = []
27
+
28
+ def print(self, *args, **kwargs):
29
+ """Print to both original console and capture buffer if capturing."""
30
+ # Always print to original console
31
+ self.original_console.print(*args, **kwargs)
32
+
33
+ if self.capture:
34
+ # Capture the output as text
35
+ # Create a temporary console to capture output
36
+ temp_output = io.StringIO()
37
+ temp_console = RichConsole(
38
+ file=temp_output,
39
+ width=self.original_console.size.width,
40
+ legacy_windows=False,
41
+ force_terminal=False,
42
+ )
43
+ temp_console.print(*args, **kwargs)
44
+ self.captured_output.append(temp_output.getvalue())
45
+
46
+ def get_captured_output(self):
47
+ """Get the captured output as plain text."""
48
+ if self.capture:
49
+ return "".join(self.captured_output)
50
+ return ""
51
+
52
+ def __getattr__(self, name):
53
+ """Delegate all other attributes to the original console."""
54
+ return getattr(self.original_console, name)
@@ -0,0 +1,82 @@
1
+ """Debug rendering utilities for verbose SSE event display.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import json
8
+ from datetime import datetime
9
+ from time import monotonic
10
+ from typing import Any
11
+
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+
16
+
17
+ def render_debug_event(
18
+ event: dict[str, Any], console: Console, started_ts: float | None = None
19
+ ) -> None:
20
+ """Render a debug panel for an SSE event.
21
+
22
+ Args:
23
+ event: The SSE event data
24
+ console: Rich console to print to
25
+ started_ts: Monotonic timestamp when streaming started
26
+ """
27
+ try:
28
+ # Add relative time since first meaningful event and wall-clock stamp
29
+ now_mono = monotonic()
30
+ rel = 0.0
31
+ if started_ts is not None:
32
+ rel = max(0.0, now_mono - started_ts)
33
+ ts_full = datetime.now().strftime("%H:%M:%S.%f")
34
+ ts_ms = ts_full[:-3] # trim to milliseconds
35
+
36
+ # Compose a descriptive title with kind/status
37
+ sse_kind = (event.get("metadata") or {}).get("kind") or "event"
38
+ status_str = event.get("status") or (event.get("metadata") or {}).get("status")
39
+ title = (
40
+ f"SSE: {sse_kind} — {status_str} @ {ts_ms} (+{rel:.2f}s)"
41
+ if status_str
42
+ else f"SSE: {sse_kind} @ {ts_ms} (+{rel:.2f}s)"
43
+ )
44
+
45
+ # Deep-pretty the event by parsing nested JSON strings
46
+ def _dejson(obj):
47
+ if isinstance(obj, dict):
48
+ return {k: _dejson(v) for k, v in obj.items()}
49
+ if isinstance(obj, list):
50
+ return [_dejson(x) for x in obj]
51
+ if isinstance(obj, str):
52
+ s = obj.strip()
53
+ if (s.startswith("{") and s.endswith("}")) or (
54
+ s.startswith("[") and s.endswith("]")
55
+ ):
56
+ try:
57
+ return _dejson(json.loads(s))
58
+ except Exception:
59
+ return obj
60
+ return obj
61
+ return obj
62
+
63
+ try:
64
+ event_json = json.dumps(_dejson(event), indent=2, ensure_ascii=False)
65
+ except Exception:
66
+ event_json = str(event)
67
+
68
+ # Choose border color by kind for readability
69
+ border = {
70
+ "agent_step": "blue",
71
+ "content": "green",
72
+ "final_response": "green",
73
+ "status": "yellow",
74
+ "artifact": "grey42",
75
+ }.get(sse_kind, "grey42")
76
+
77
+ # Render using Markdown with JSON code block (consistent with tool panels)
78
+ md = Markdown(f"```json\n{event_json}\n```", code_theme="monokai")
79
+ console.print(Panel(md, title=title, border_style=border))
80
+ except Exception as e:
81
+ # Debug helpers must not break streaming
82
+ print(f"Debug error: {e}") # Fallback debug output
@@ -0,0 +1,123 @@
1
+ """Panel rendering utilities for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.markdown import Markdown
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+
13
+
14
+ def create_main_panel(content: str, title: str, theme: str = "dark") -> Panel:
15
+ """Create a main content panel.
16
+
17
+ Args:
18
+ content: The content to display
19
+ title: Panel title
20
+ theme: Color theme ("dark" or "light")
21
+
22
+ Returns:
23
+ Rich Panel instance
24
+ """
25
+ if content.strip():
26
+ return Panel(
27
+ Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
28
+ title=title,
29
+ border_style="green",
30
+ )
31
+ else:
32
+ # Placeholder panel
33
+ placeholder = Text("Processing...", style="dim")
34
+ return Panel(
35
+ placeholder,
36
+ title=title,
37
+ border_style="green",
38
+ )
39
+
40
+
41
+ def create_tool_panel(
42
+ title: str,
43
+ content: str,
44
+ status: str = "running",
45
+ theme: str = "dark",
46
+ is_delegation: bool = False,
47
+ ) -> Panel:
48
+ """Create a tool execution panel.
49
+
50
+ Args:
51
+ title: Tool name/title
52
+ content: Tool output content
53
+ status: Tool execution status
54
+ theme: Color theme
55
+ is_delegation: Whether this is a delegation tool
56
+
57
+ Returns:
58
+ Rich Panel instance
59
+ """
60
+ mark = "✓" if status == "finished" else "⟳"
61
+ border_style = "magenta" if is_delegation else "blue"
62
+
63
+ return Panel(
64
+ Markdown(
65
+ content or "Processing...",
66
+ code_theme=("monokai" if theme == "dark" else "github"),
67
+ ),
68
+ title=f"{title} {mark}",
69
+ border_style=border_style,
70
+ )
71
+
72
+
73
+ def create_context_panel(
74
+ title: str,
75
+ content: str,
76
+ status: str = "running",
77
+ theme: str = "dark",
78
+ is_delegation: bool = False,
79
+ ) -> Panel:
80
+ """Create a context/sub-agent panel.
81
+
82
+ Args:
83
+ title: Context title
84
+ content: Context content
85
+ status: Execution status
86
+ theme: Color theme
87
+ is_delegation: Whether this is a delegation context
88
+
89
+ Returns:
90
+ Rich Panel instance
91
+ """
92
+ mark = "✓" if status == "finished" else "⟳"
93
+ border_style = "magenta" if is_delegation else "cyan"
94
+
95
+ return Panel(
96
+ Markdown(
97
+ content,
98
+ code_theme=("monokai" if theme == "dark" else "github"),
99
+ ),
100
+ title=f"{title} {mark}",
101
+ border_style=border_style,
102
+ )
103
+
104
+
105
+ def create_final_panel(
106
+ content: str, title: str = "Final Result", theme: str = "dark"
107
+ ) -> Panel:
108
+ """Create a final result panel.
109
+
110
+ Args:
111
+ content: Final result content
112
+ title: Panel title
113
+ theme: Color theme
114
+
115
+ Returns:
116
+ Rich Panel instance
117
+ """
118
+ return Panel(
119
+ Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
120
+ title=title,
121
+ border_style="green",
122
+ padding=(0, 1),
123
+ )
@@ -0,0 +1,118 @@
1
+ """Progress and timing utilities for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from time import monotonic
10
+
11
+ from glaip_sdk.utils.rendering.formatting import get_spinner_char
12
+
13
+
14
+ def get_spinner() -> str:
15
+ """Return the current animated spinner character for visual feedback."""
16
+ return get_spinner_char()
17
+
18
+
19
+ def format_working_indicator(
20
+ started_at: float | None,
21
+ server_elapsed_time: float | None = None,
22
+ streaming_started_at: float | None = None,
23
+ ) -> str:
24
+ """Format a working indicator with elapsed time.
25
+
26
+ Args:
27
+ started_at: Timestamp when work started, or None
28
+ server_elapsed_time: Server-reported elapsed time if available
29
+ streaming_started_at: When streaming started
30
+
31
+ Returns:
32
+ Formatted working indicator string with elapsed time
33
+ """
34
+ chip = "Working..."
35
+
36
+ # Use server timing if available (more accurate)
37
+ if server_elapsed_time is not None and streaming_started_at is not None:
38
+ elapsed = server_elapsed_time
39
+ elif started_at:
40
+ try:
41
+ elapsed = monotonic() - started_at
42
+ except Exception:
43
+ return chip
44
+ else:
45
+ return chip
46
+
47
+ if elapsed >= 1:
48
+ chip = f"Working... ({elapsed:.2f}s)"
49
+ else:
50
+ elapsed_ms = int(elapsed * 1000)
51
+ chip = f"Working... ({elapsed_ms}ms)" if elapsed_ms > 0 else "Working... (<1ms)"
52
+ return chip
53
+
54
+
55
+ def format_elapsed_time(elapsed_seconds: float) -> str:
56
+ """Format elapsed time in a human-readable format.
57
+
58
+ Args:
59
+ elapsed_seconds: Time in seconds
60
+
61
+ Returns:
62
+ Formatted time string
63
+ """
64
+ if elapsed_seconds >= 60:
65
+ minutes = int(elapsed_seconds // 60)
66
+ seconds = elapsed_seconds % 60
67
+ return f"{minutes}m {seconds:.1f}s"
68
+ elif elapsed_seconds >= 1:
69
+ return f"{elapsed_seconds:.2f}s"
70
+ else:
71
+ ms = int(elapsed_seconds * 1000)
72
+ return f"{ms}ms" if ms > 0 else "<1ms"
73
+
74
+
75
+ def is_delegation_tool(tool_name: str) -> bool:
76
+ """Check if a tool name indicates delegation functionality.
77
+
78
+ Args:
79
+ tool_name: The name of the tool to check
80
+
81
+ Returns:
82
+ True if this is a delegation tool
83
+ """
84
+ return (
85
+ tool_name.startswith("delegate_to_")
86
+ or tool_name.startswith("delegate_")
87
+ or "sub_agent" in tool_name.lower()
88
+ )
89
+
90
+
91
+ def format_tool_title(tool_name: str) -> str:
92
+ """Format tool name for panel title display.
93
+
94
+ Args:
95
+ tool_name: The full tool name (may include file paths)
96
+
97
+ Returns:
98
+ Formatted title string suitable for panel display
99
+ """
100
+ # Check if this is a delegation tool
101
+ if is_delegation_tool(tool_name):
102
+ # Extract the sub-agent name from delegation tool names
103
+ if tool_name.startswith("delegate_to_"):
104
+ sub_agent_name = tool_name.replace("delegate_to_", "")
105
+ return f"Sub-Agent: {sub_agent_name}"
106
+ elif tool_name.startswith("delegate_"):
107
+ sub_agent_name = tool_name.replace("delegate_", "")
108
+ return f"Sub-Agent: {sub_agent_name}"
109
+
110
+ # For regular tools, clean up the name
111
+ # Remove file path prefixes if present
112
+ if "/" in tool_name:
113
+ tool_name = tool_name.split("/")[-1]
114
+ if "." in tool_name:
115
+ tool_name = tool_name.split(".")[0]
116
+
117
+ # Convert snake_case to Title Case
118
+ return tool_name.replace("_", " ").title()
@@ -0,0 +1,198 @@
1
+ """Event routing and parsing utilities for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from time import monotonic
10
+ from typing import Any
11
+
12
+
13
+ class StreamProcessor:
14
+ """Handles event routing and parsing for streaming agent execution."""
15
+
16
+ def __init__(self):
17
+ """Initialize the stream processor."""
18
+ self.streaming_started_at: float | None = None
19
+ self.server_elapsed_time: float | None = None
20
+ self.current_event_tools: set[str] = set()
21
+ self.current_event_sub_agents: set[str] = set()
22
+ self.current_event_finished_panels: set[str] = set()
23
+ self.last_event_time_by_ctx: dict[str, float] = {}
24
+
25
+ def reset_event_tracking(self):
26
+ """Reset tracking for the current event."""
27
+ self.current_event_tools.clear()
28
+ self.current_event_sub_agents.clear()
29
+ self.current_event_finished_panels.clear()
30
+
31
+ def extract_event_metadata(self, event: dict[str, Any]) -> dict[str, Any]:
32
+ """Extract metadata from an event.
33
+
34
+ Args:
35
+ event: Event dictionary
36
+
37
+ Returns:
38
+ Dictionary with extracted metadata
39
+ """
40
+ metadata = event.get("metadata", {})
41
+ # Update server elapsed timing if backend provides it
42
+ try:
43
+ t = metadata.get("time")
44
+ if isinstance(t, int | float):
45
+ self.server_elapsed_time = float(t)
46
+ except Exception:
47
+ pass
48
+
49
+ return {
50
+ "kind": metadata.get("kind") if metadata else None,
51
+ "task_id": event.get("task_id"),
52
+ "context_id": event.get("context_id"),
53
+ "content": event.get("content", ""),
54
+ "status": metadata.get("status") if metadata else event.get("status"),
55
+ "metadata": metadata,
56
+ }
57
+
58
+ def parse_tool_calls(
59
+ self, event: dict[str, Any]
60
+ ) -> tuple[str | None, Any, Any, list]:
61
+ """Parse tool call information from an event.
62
+
63
+ Args:
64
+ event: Event dictionary
65
+
66
+ Returns:
67
+ Tuple of (tool_name, tool_args, tool_output, tool_calls_info)
68
+ """
69
+ tool_name = None
70
+ tool_args = {}
71
+ tool_out = None
72
+ tool_calls_info = []
73
+
74
+ # Extract tool information from metadata
75
+ metadata = event.get("metadata", {})
76
+ tool_calls = metadata.get("tool_calls", [])
77
+
78
+ if tool_calls:
79
+ # Take the first tool call if multiple exist
80
+ first_call = tool_calls[0] if isinstance(tool_calls, list) else tool_calls
81
+ tool_name = first_call.get("name")
82
+ tool_args = first_call.get("arguments", {})
83
+ tool_out = first_call.get("output")
84
+
85
+ # Collect info for all tool calls
86
+ for call in tool_calls if isinstance(tool_calls, list) else [tool_calls]:
87
+ if isinstance(call, dict) and "name" in call:
88
+ tool_calls_info.append(
89
+ (
90
+ call.get("name", ""),
91
+ call.get("arguments", {}),
92
+ call.get("output"),
93
+ )
94
+ )
95
+
96
+ # Fallback to nested metadata.tool_info (newer schema)
97
+ if not tool_calls_info:
98
+ tool_info = metadata.get("tool_info", {}) or {}
99
+ # Case 1: tool_info.tool_calls
100
+ ti_calls = tool_info.get("tool_calls")
101
+ if isinstance(ti_calls, list) and ti_calls:
102
+ for call in ti_calls:
103
+ if isinstance(call, dict) and call.get("name"):
104
+ tool_calls_info.append(
105
+ (call.get("name"), call.get("args", {}), call.get("output"))
106
+ )
107
+ if tool_calls_info and not tool_name:
108
+ tool_name, tool_args, tool_out = tool_calls_info[0]
109
+ # Case 2: single tool_info name/args/output
110
+ if tool_info.get("name") and not tool_name:
111
+ tool_name = tool_info.get("name")
112
+ tool_args = tool_info.get("args", {})
113
+ tool_out = tool_info.get("output")
114
+ tool_calls_info.append((tool_name, tool_args, tool_out))
115
+
116
+ return tool_name, tool_args, tool_out, tool_calls_info
117
+
118
+ def update_timing(self, context_id: str | None):
119
+ """Update timing information for the given context.
120
+
121
+ Args:
122
+ context_id: Context identifier
123
+ """
124
+ if context_id:
125
+ self.last_event_time_by_ctx[context_id] = monotonic()
126
+
127
+ def should_insert_thinking_gap(
128
+ self, task_id: str | None, context_id: str | None, think_threshold: float
129
+ ) -> bool:
130
+ """Determine if a thinking gap should be inserted.
131
+
132
+ Args:
133
+ task_id: Task identifier
134
+ context_id: Context identifier
135
+ think_threshold: Threshold for thinking gap
136
+
137
+ Returns:
138
+ True if thinking gap should be inserted
139
+ """
140
+ if not task_id or not context_id:
141
+ return False
142
+
143
+ last_time = self.last_event_time_by_ctx.get(context_id)
144
+ if last_time is None:
145
+ return True
146
+
147
+ elapsed = monotonic() - last_time
148
+ return elapsed >= think_threshold
149
+
150
+ def track_tools_and_agents(
151
+ self, tool_name: str | None, tool_calls_info: list, is_delegation_tool_func
152
+ ):
153
+ """Track tools and sub-agents mentioned in the current event.
154
+
155
+ Args:
156
+ tool_name: Primary tool name
157
+ tool_calls_info: List of tool call information
158
+ is_delegation_tool_func: Function to check if tool is delegation
159
+ """
160
+ # Track all tools mentioned in this event
161
+ if tool_name:
162
+ self.current_event_tools.add(tool_name)
163
+ # If it's a delegation tool, add the sub-agent name
164
+ if is_delegation_tool_func(tool_name):
165
+ sub_agent_name = self._extract_sub_agent_name(tool_name)
166
+ self.current_event_sub_agents.add(sub_agent_name)
167
+
168
+ if tool_calls_info:
169
+ for tool_call_name, _, _ in tool_calls_info:
170
+ self.current_event_tools.add(tool_call_name)
171
+ # If it's a delegation tool, add the sub-agent name
172
+ if is_delegation_tool_func(tool_call_name):
173
+ sub_agent_name = self._extract_sub_agent_name(tool_call_name)
174
+ self.current_event_sub_agents.add(sub_agent_name)
175
+
176
+ def _extract_sub_agent_name(self, tool_name: str) -> str:
177
+ """Extract sub-agent name from delegation tool name.
178
+
179
+ Args:
180
+ tool_name: Delegation tool name
181
+
182
+ Returns:
183
+ Sub-agent name
184
+ """
185
+ if tool_name.startswith("delegate_to_"):
186
+ return tool_name.replace("delegate_to_", "")
187
+ elif tool_name.startswith("delegate_"):
188
+ return tool_name.replace("delegate_", "")
189
+ else:
190
+ return tool_name
191
+
192
+ def get_current_event_tools(self) -> set[str]:
193
+ """Get the set of tools mentioned in the current event."""
194
+ return self.current_event_tools.copy()
195
+
196
+ def get_current_event_sub_agents(self) -> set[str]:
197
+ """Get the set of sub-agents mentioned in the current event."""
198
+ return self.current_event_sub_agents.copy()