glaip-sdk 0.1.3__py3-none-any.whl → 0.6.19__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.
- glaip_sdk/__init__.py +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +241 -121
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +291 -35
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +466 -89
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/hitl/__init__.py +15 -0
- glaip_sdk/hitl/local.py +151 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +870 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +275 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +56 -21
- glaip_sdk-0.6.19.dist-info/RECORD +163 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- glaip_sdk-0.1.3.dist-info/entry_points.txt +0 -3
glaip_sdk/client/tools.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
@@ -16,11 +17,14 @@ from glaip_sdk.config.constants import (
|
|
|
16
17
|
DEFAULT_TOOL_TYPE,
|
|
17
18
|
DEFAULT_TOOL_VERSION,
|
|
18
19
|
)
|
|
19
|
-
from glaip_sdk.models import
|
|
20
|
+
from glaip_sdk.models import ToolResponse
|
|
21
|
+
from glaip_sdk.tools import Tool
|
|
20
22
|
from glaip_sdk.utils.client_utils import (
|
|
23
|
+
add_kwargs_to_payload,
|
|
21
24
|
create_model_instances,
|
|
22
25
|
find_by_name,
|
|
23
26
|
)
|
|
27
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
24
28
|
|
|
25
29
|
# API endpoints
|
|
26
30
|
TOOLS_ENDPOINT = "/tools/"
|
|
@@ -58,11 +62,11 @@ class ToolClient(BaseClient):
|
|
|
58
62
|
def get_tool_by_id(self, tool_id: str) -> Tool:
|
|
59
63
|
"""Get tool by ID."""
|
|
60
64
|
data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
61
|
-
|
|
65
|
+
response = ToolResponse(**data)
|
|
66
|
+
return Tool.from_response(response, client=self)
|
|
62
67
|
|
|
63
68
|
def find_tools(self, name: str | None = None) -> list[Tool]:
|
|
64
69
|
"""Find tools by name."""
|
|
65
|
-
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
66
70
|
data = self._request("GET", TOOLS_ENDPOINT)
|
|
67
71
|
tools = create_model_instances(data, Tool, self)
|
|
68
72
|
return find_by_name(tools, name, case_sensitive=False)
|
|
@@ -111,6 +115,7 @@ class ToolClient(BaseClient):
|
|
|
111
115
|
data = {
|
|
112
116
|
"name": name,
|
|
113
117
|
"framework": framework,
|
|
118
|
+
"type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
if description:
|
|
@@ -152,7 +157,8 @@ class ToolClient(BaseClient):
|
|
|
152
157
|
data=upload_data,
|
|
153
158
|
)
|
|
154
159
|
|
|
155
|
-
|
|
160
|
+
tool_response = ToolResponse(**response)
|
|
161
|
+
return Tool.from_response(tool_response, client=self)
|
|
156
162
|
|
|
157
163
|
def _build_create_payload(
|
|
158
164
|
self,
|
|
@@ -200,9 +206,7 @@ class ToolClient(BaseClient):
|
|
|
200
206
|
|
|
201
207
|
# Add any other kwargs (excluding already handled ones)
|
|
202
208
|
excluded_keys = {"tags", "version"}
|
|
203
|
-
|
|
204
|
-
if key not in excluded_keys:
|
|
205
|
-
payload[key] = value
|
|
209
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
206
210
|
|
|
207
211
|
return payload
|
|
208
212
|
|
|
@@ -276,6 +280,9 @@ class ToolClient(BaseClient):
|
|
|
276
280
|
or getattr(current_tool, "type", None)
|
|
277
281
|
or DEFAULT_TOOL_TYPE
|
|
278
282
|
)
|
|
283
|
+
# Convert enum to string value for API payload
|
|
284
|
+
if hasattr(current_type, "value"):
|
|
285
|
+
current_type = current_type.value
|
|
279
286
|
|
|
280
287
|
update_data = {
|
|
281
288
|
"name": name if name is not None else current_tool.name,
|
|
@@ -435,12 +442,149 @@ class ToolClient(BaseClient):
|
|
|
435
442
|
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
436
443
|
"""Update an existing tool."""
|
|
437
444
|
data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
|
|
438
|
-
|
|
445
|
+
response = ToolResponse(**data)
|
|
446
|
+
return Tool.from_response(response, client=self)
|
|
439
447
|
|
|
440
448
|
def delete_tool(self, tool_id: str) -> None:
|
|
441
449
|
"""Delete a tool."""
|
|
442
450
|
self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
443
451
|
|
|
452
|
+
def upsert_tool(
|
|
453
|
+
self,
|
|
454
|
+
identifier: str | Tool,
|
|
455
|
+
code: str | None = None,
|
|
456
|
+
description: str | None = None,
|
|
457
|
+
framework: str = "langchain",
|
|
458
|
+
**kwargs,
|
|
459
|
+
) -> Tool:
|
|
460
|
+
"""Create or update a tool by instance, ID, or name.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
identifier: Tool instance, ID (UUID string), or name
|
|
464
|
+
code: Python code containing the tool plugin (required for create)
|
|
465
|
+
description: Tool description
|
|
466
|
+
framework: Tool framework (defaults to "langchain")
|
|
467
|
+
**kwargs: Additional parameters (tags, version, etc.)
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
The created or updated tool.
|
|
471
|
+
|
|
472
|
+
Example:
|
|
473
|
+
>>> # By name with code (creates if not exists)
|
|
474
|
+
>>> tool = client.tools.upsert_tool(
|
|
475
|
+
... "greeting",
|
|
476
|
+
... code=bundled_source,
|
|
477
|
+
... description="A greeting tool",
|
|
478
|
+
... )
|
|
479
|
+
>>> # By instance
|
|
480
|
+
>>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
|
|
481
|
+
>>> # By ID
|
|
482
|
+
>>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
|
|
483
|
+
"""
|
|
484
|
+
# Handle Tool instance
|
|
485
|
+
if isinstance(identifier, Tool):
|
|
486
|
+
if identifier.id:
|
|
487
|
+
logger.info("Updating tool by instance: %s", identifier.name)
|
|
488
|
+
return self._do_tool_upsert_update(
|
|
489
|
+
identifier.id,
|
|
490
|
+
identifier.name,
|
|
491
|
+
code,
|
|
492
|
+
description,
|
|
493
|
+
framework,
|
|
494
|
+
**kwargs,
|
|
495
|
+
)
|
|
496
|
+
identifier = identifier.name
|
|
497
|
+
|
|
498
|
+
# Handle string (ID or name)
|
|
499
|
+
if isinstance(identifier, str):
|
|
500
|
+
if is_uuid(identifier):
|
|
501
|
+
logger.info("Updating tool by ID: %s", identifier)
|
|
502
|
+
existing = self.get_tool_by_id(identifier)
|
|
503
|
+
return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
|
|
504
|
+
|
|
505
|
+
# It's a name - find or create
|
|
506
|
+
return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
|
|
507
|
+
|
|
508
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
509
|
+
|
|
510
|
+
def _do_tool_upsert_update(
|
|
511
|
+
self,
|
|
512
|
+
tool_id: str,
|
|
513
|
+
name: str | None,
|
|
514
|
+
code: str | None,
|
|
515
|
+
description: str | None,
|
|
516
|
+
framework: str,
|
|
517
|
+
**kwargs,
|
|
518
|
+
) -> Tool:
|
|
519
|
+
"""Perform the update part of tool upsert."""
|
|
520
|
+
if code:
|
|
521
|
+
# Update via file upload
|
|
522
|
+
with tempfile.NamedTemporaryFile(
|
|
523
|
+
mode="w",
|
|
524
|
+
suffix=".py",
|
|
525
|
+
prefix=f"{name or 'tool'}_",
|
|
526
|
+
delete=False,
|
|
527
|
+
encoding="utf-8",
|
|
528
|
+
) as temp_file:
|
|
529
|
+
temp_file.write(code)
|
|
530
|
+
temp_file_path = temp_file.name
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
return self.update_tool_via_file(
|
|
534
|
+
tool_id,
|
|
535
|
+
temp_file_path,
|
|
536
|
+
name=name,
|
|
537
|
+
description=description,
|
|
538
|
+
framework=framework,
|
|
539
|
+
**kwargs,
|
|
540
|
+
)
|
|
541
|
+
finally:
|
|
542
|
+
try:
|
|
543
|
+
os.unlink(temp_file_path)
|
|
544
|
+
except OSError:
|
|
545
|
+
pass
|
|
546
|
+
else:
|
|
547
|
+
# Metadata-only update
|
|
548
|
+
update_kwargs = {"framework": framework, **kwargs}
|
|
549
|
+
if name:
|
|
550
|
+
update_kwargs["name"] = name
|
|
551
|
+
if description:
|
|
552
|
+
update_kwargs["description"] = description
|
|
553
|
+
return self.update_tool(tool_id, **update_kwargs)
|
|
554
|
+
|
|
555
|
+
def _upsert_tool_by_name(
|
|
556
|
+
self,
|
|
557
|
+
name: str,
|
|
558
|
+
code: str | None,
|
|
559
|
+
description: str | None,
|
|
560
|
+
framework: str,
|
|
561
|
+
**kwargs,
|
|
562
|
+
) -> Tool:
|
|
563
|
+
"""Find tool by name and update, or create if not found."""
|
|
564
|
+
existing = self.find_tools(name)
|
|
565
|
+
name_lower = name.lower()
|
|
566
|
+
exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
|
|
567
|
+
|
|
568
|
+
if len(exact_matches) == 1:
|
|
569
|
+
logger.info("Updating existing tool: %s", name)
|
|
570
|
+
return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
|
|
571
|
+
|
|
572
|
+
if len(exact_matches) > 1:
|
|
573
|
+
raise ValueError(f"Multiple tools found with name '{name}'")
|
|
574
|
+
|
|
575
|
+
# Create new tool - code is required
|
|
576
|
+
if not code:
|
|
577
|
+
raise ValueError(f"Tool '{name}' not found and no code provided for creation")
|
|
578
|
+
|
|
579
|
+
logger.info("Creating new tool: %s", name)
|
|
580
|
+
return self.create_tool_from_code(
|
|
581
|
+
name=name,
|
|
582
|
+
code=code,
|
|
583
|
+
framework=framework,
|
|
584
|
+
description=description,
|
|
585
|
+
**kwargs,
|
|
586
|
+
)
|
|
587
|
+
|
|
444
588
|
def get_tool_script(self, tool_id: str) -> str:
|
|
445
589
|
"""Get the tool script content.
|
|
446
590
|
|
|
@@ -509,8 +653,9 @@ class ToolClient(BaseClient):
|
|
|
509
653
|
data=update_payload,
|
|
510
654
|
)
|
|
511
655
|
|
|
512
|
-
|
|
656
|
+
tool_response = ToolResponse(**response)
|
|
657
|
+
return Tool.from_response(tool_response, client=self)
|
|
513
658
|
|
|
514
659
|
except Exception as e:
|
|
515
|
-
logger.error(
|
|
660
|
+
logger.error("Failed to update tool %s via file: %s", tool_id, e)
|
|
516
661
|
raise
|
glaip_sdk/config/constants.py
CHANGED
|
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
|
|
|
39
39
|
|
|
40
40
|
# Default error messages
|
|
41
41
|
DEFAULT_ERROR_MESSAGE = "Unknown error"
|
|
42
|
+
|
|
43
|
+
# Agent configuration fields used for CLI args and payload building
|
|
44
|
+
AGENT_CONFIG_FIELDS = (
|
|
45
|
+
"name",
|
|
46
|
+
"instruction",
|
|
47
|
+
"model",
|
|
48
|
+
"tools",
|
|
49
|
+
"agents",
|
|
50
|
+
"mcps",
|
|
51
|
+
"timeout",
|
|
52
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Human-in-the-Loop (HITL) utilities for glaip-sdk.
|
|
2
|
+
|
|
3
|
+
This package provides utilities for HITL approval workflows in both local
|
|
4
|
+
and remote agent execution modes.
|
|
5
|
+
|
|
6
|
+
For local development, LocalPromptHandler is automatically injected when
|
|
7
|
+
agent_config.hitl_enabled is True. No manual setup required.
|
|
8
|
+
|
|
9
|
+
Authors:
|
|
10
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.hitl.local import LocalPromptHandler, PauseResumeCallback
|
|
14
|
+
|
|
15
|
+
__all__ = ["LocalPromptHandler", "PauseResumeCallback"]
|
glaip_sdk/hitl/local.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Local HITL prompt handler with interactive console support.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from aip_agents.agent.hitl.prompt.base import BasePromptHandler
|
|
12
|
+
from aip_agents.schema.hitl import ApprovalDecision, ApprovalDecisionType, ApprovalRequest
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
raise ImportError("aip_agents is required for local HITL. Install with: pip install 'glaip-sdk[local]'") from e
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LocalPromptHandler(BasePromptHandler):
|
|
21
|
+
"""Local HITL prompt handler with interactive console prompts.
|
|
22
|
+
|
|
23
|
+
Experimental local HITL implementation with known limitations:
|
|
24
|
+
- Timeouts are not enforced (interactive prompts wait indefinitely)
|
|
25
|
+
- Relies on private renderer methods for pause/resume
|
|
26
|
+
- Only supports interactive terminal environments
|
|
27
|
+
|
|
28
|
+
The key insight from Rich documentation is that Live must be stopped before
|
|
29
|
+
using Prompt/input(), otherwise the input won't render properly.
|
|
30
|
+
|
|
31
|
+
Environment variables:
|
|
32
|
+
GLAIP_HITL_AUTO_APPROVE: Set to "true" (case-insensitive) to auto-approve
|
|
33
|
+
all requests without user interaction. Useful for integration tests and CI.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, pause_resume_callback: Any | None = None) -> None:
|
|
37
|
+
"""Initialize the prompt handler.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
pause_resume_callback: Optional callable with pause() and resume() methods
|
|
41
|
+
to control the live renderer during prompts. This is needed because
|
|
42
|
+
Rich Live interferes with Prompt/input().
|
|
43
|
+
"""
|
|
44
|
+
super().__init__()
|
|
45
|
+
self._pause_resume = pause_resume_callback
|
|
46
|
+
self._console = Console()
|
|
47
|
+
|
|
48
|
+
async def prompt_for_decision(
|
|
49
|
+
self,
|
|
50
|
+
request: ApprovalRequest,
|
|
51
|
+
timeout_seconds: int,
|
|
52
|
+
context_keys: list[str] | None = None,
|
|
53
|
+
) -> ApprovalDecision:
|
|
54
|
+
"""Prompt for approval decision with live renderer pause/resume.
|
|
55
|
+
|
|
56
|
+
Supports auto-approval via GLAIP_HITL_AUTO_APPROVE environment variable
|
|
57
|
+
for integration testing and CI environments. Set to "true" (case-insensitive) to enable.
|
|
58
|
+
"""
|
|
59
|
+
_ = (timeout_seconds, context_keys) # Suppress unused parameter warnings.
|
|
60
|
+
|
|
61
|
+
# Check for auto-approve mode (for integration tests/CI)
|
|
62
|
+
auto_approve = os.getenv("GLAIP_HITL_AUTO_APPROVE", "").lower() == "true"
|
|
63
|
+
|
|
64
|
+
if auto_approve:
|
|
65
|
+
# Auto-approve without user interaction
|
|
66
|
+
return ApprovalDecision(
|
|
67
|
+
request_id=request.request_id,
|
|
68
|
+
decision=ApprovalDecisionType.APPROVED,
|
|
69
|
+
operator_input="auto-approved",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Pause the live renderer if callback is available
|
|
73
|
+
if self._pause_resume:
|
|
74
|
+
self._pause_resume.pause()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# POC/MVP: Show what we're approving (still auto-approve for now)
|
|
78
|
+
self._print_request_info(request)
|
|
79
|
+
|
|
80
|
+
# POC/MVP: For testing, we can do actual input here
|
|
81
|
+
# Uncomment to enable real prompting:
|
|
82
|
+
response = Prompt.ask(
|
|
83
|
+
"\n[yellow]Approve this tool call?[/yellow] [dim](y/n/s)[/dim]",
|
|
84
|
+
console=self._console,
|
|
85
|
+
default="y",
|
|
86
|
+
)
|
|
87
|
+
response = response.lower().strip()
|
|
88
|
+
|
|
89
|
+
if response in ("y", "yes"):
|
|
90
|
+
decision = ApprovalDecisionType.APPROVED
|
|
91
|
+
elif response in ("n", "no"):
|
|
92
|
+
decision = ApprovalDecisionType.REJECTED
|
|
93
|
+
else:
|
|
94
|
+
decision = ApprovalDecisionType.SKIPPED
|
|
95
|
+
|
|
96
|
+
return ApprovalDecision(
|
|
97
|
+
request_id=request.request_id,
|
|
98
|
+
decision=decision,
|
|
99
|
+
operator_input=response if decision != ApprovalDecisionType.SKIPPED else None,
|
|
100
|
+
)
|
|
101
|
+
finally:
|
|
102
|
+
# Always resume the live renderer
|
|
103
|
+
if self._pause_resume:
|
|
104
|
+
self._pause_resume.resume()
|
|
105
|
+
|
|
106
|
+
def _print_request_info(self, request: ApprovalRequest) -> None:
|
|
107
|
+
"""Print the approval request information."""
|
|
108
|
+
self._console.print()
|
|
109
|
+
self._console.rule("[yellow]HITL Approval Request[/yellow]", style="yellow")
|
|
110
|
+
|
|
111
|
+
tool_name = request.tool_name or "unknown"
|
|
112
|
+
self._console.print(f"[cyan]Tool:[/cyan] {tool_name}")
|
|
113
|
+
|
|
114
|
+
if hasattr(request, "arguments_preview") and request.arguments_preview:
|
|
115
|
+
self._console.print(f"[cyan]Arguments:[/cyan] {request.arguments_preview}")
|
|
116
|
+
|
|
117
|
+
if request.context:
|
|
118
|
+
self._console.print(f"[dim]Context: {request.context}[/dim]")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PauseResumeCallback:
|
|
122
|
+
"""Simple callback object for pausing/resuming the live renderer.
|
|
123
|
+
|
|
124
|
+
This allows the LocalPromptHandler to control the renderer without
|
|
125
|
+
directly coupling to the renderer implementation.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self) -> None:
|
|
129
|
+
"""Initialize the callback."""
|
|
130
|
+
self._renderer: Any | None = None
|
|
131
|
+
|
|
132
|
+
def set_renderer(self, renderer: Any) -> None:
|
|
133
|
+
"""Set the renderer instance.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
renderer: RichStreamRenderer instance with pause_live() and resume_live() methods.
|
|
137
|
+
"""
|
|
138
|
+
self._renderer = renderer
|
|
139
|
+
|
|
140
|
+
def pause(self) -> None:
|
|
141
|
+
"""Pause the live renderer before prompting."""
|
|
142
|
+
if self._renderer and hasattr(self._renderer, "_shutdown_live"):
|
|
143
|
+
self._renderer._shutdown_live()
|
|
144
|
+
|
|
145
|
+
def resume(self) -> None:
|
|
146
|
+
"""Resume the live renderer after prompting."""
|
|
147
|
+
if self._renderer and hasattr(self._renderer, "_ensure_live"):
|
|
148
|
+
self._renderer._ensure_live()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = ["LocalPromptHandler", "PauseResumeCallback"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) package for GL AIP platform.
|
|
2
|
+
|
|
3
|
+
This package provides the MCP class and MCPRegistry for managing
|
|
4
|
+
Model Context Protocol configurations on the GL AIP platform.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from glaip_sdk.mcps import MCP, get_mcp_registry
|
|
8
|
+
>>> mcp = MCP.from_native("arxiv-search")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.mcps.base import MCP, MCPConfigValue
|
|
14
|
+
from glaip_sdk.registry.mcp import MCPRegistry, get_mcp_registry
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MCP",
|
|
18
|
+
"MCPConfigValue",
|
|
19
|
+
"MCPRegistry",
|
|
20
|
+
"get_mcp_registry",
|
|
21
|
+
]
|