alita-sdk 0.3.462__py3-none-any.whl → 0.3.627__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.
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +15 -3
- alita_sdk/cli/agent_loader.py +56 -8
- alita_sdk/cli/agent_ui.py +93 -31
- alita_sdk/cli/agents.py +2274 -230
- alita_sdk/cli/callbacks.py +96 -25
- alita_sdk/cli/cli.py +10 -1
- alita_sdk/cli/config.py +162 -9
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +14 -17
- alita_sdk/cli/toolkit_loader.py +35 -5
- alita_sdk/cli/tools/__init__.py +36 -2
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +910 -64
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +0 -3
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +17 -5
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +353 -48
- alita_sdk/runtime/clients/sandbox_client.py +0 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +123 -26
- alita_sdk/runtime/langchain/constants.py +642 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +279 -73
- alita_sdk/runtime/langchain/utils.py +82 -15
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +7 -0
- alita_sdk/runtime/toolkits/application.py +21 -9
- alita_sdk/runtime/toolkits/artifact.py +15 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +139 -251
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +238 -32
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/application.py +20 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +43 -15
- alita_sdk/runtime/tools/image_generation.py +50 -44
- alita_sdk/runtime/tools/llm.py +852 -67
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +9 -6
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +7 -2
- alita_sdk/runtime/tools/vectorstore_base.py +51 -11
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +202 -5
- alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +6 -10
- alita_sdk/runtime/utils/toolkit_utils.py +16 -5
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +113 -29
- alita_sdk/tools/ado/repos/__init__.py +51 -33
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -9
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +170 -45
- alita_sdk/tools/bitbucket/__init__.py +17 -12
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +10 -7
- alita_sdk/tools/code_indexer_toolkit.py +73 -23
- alita_sdk/tools/confluence/__init__.py +21 -15
- alita_sdk/tools/confluence/api_wrapper.py +78 -23
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +13 -14
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +15 -11
- alita_sdk/tools/gitlab/api_wrapper.py +207 -41
- alita_sdk/tools/gitlab_org/__init__.py +10 -8
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +10 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -11
- alita_sdk/tools/jira/api_wrapper.py +91 -40
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +11 -3
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +490 -114
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/pptx/__init__.py +10 -9
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +30 -10
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +10 -8
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -9
- alita_sdk/tools/salesforce/__init__.py +10 -9
- alita_sdk/tools/servicenow/__init__.py +17 -14
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -8
- alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
- alita_sdk/tools/slack/__init__.py +10 -8
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +11 -9
- alita_sdk/tools/testio/__init__.py +10 -8
- alita_sdk/tools/testrail/__init__.py +11 -8
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
- alita_sdk/tools/xray/__init__.py +12 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +9 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
- alita_sdk/tools/zephyr_essential/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -9
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -8
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.462.dist-info/RECORD +0 -384
- alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
alita_sdk/runtime/tools/loop.py
CHANGED
|
@@ -102,7 +102,9 @@ Input Data:
|
|
|
102
102
|
logger.debug(f"LoopNode input: {predict_input}")
|
|
103
103
|
completion = self.client.invoke(predict_input, config=config)
|
|
104
104
|
logger.debug(f"LoopNode pure output: {completion}")
|
|
105
|
-
|
|
105
|
+
from ..langchain.utils import extract_text_from_completion
|
|
106
|
+
content_text = extract_text_from_completion(completion)
|
|
107
|
+
loop_data = _old_extract_json(content_text.strip())
|
|
106
108
|
logger.debug(f"LoopNode output: {loop_data}")
|
|
107
109
|
if self.return_type == "str":
|
|
108
110
|
accumulated_response = ''
|
|
@@ -93,7 +93,9 @@ Answer must be JSON only extractable by JSON.LOADS."""
|
|
|
93
93
|
else:
|
|
94
94
|
input_[-1].content += self.unstructured_output
|
|
95
95
|
completion = self.client.invoke(input_, config=config)
|
|
96
|
-
|
|
96
|
+
from ..langchain.utils import extract_text_from_completion
|
|
97
|
+
content_text = extract_text_from_completion(completion)
|
|
98
|
+
result = _extract_json(content_text.strip())
|
|
97
99
|
try:
|
|
98
100
|
tool_result: dict | List[dict] = self.tool.invoke(result, config=config, kwargs=kwargs)
|
|
99
101
|
dispatch_custom_event(
|
|
@@ -20,10 +20,14 @@ from ..utils.mcp_oauth import (
|
|
|
20
20
|
fetch_resource_metadata_async,
|
|
21
21
|
infer_authorization_servers_from_realm,
|
|
22
22
|
)
|
|
23
|
-
from ..utils.
|
|
23
|
+
from ..utils.mcp_client import McpClient
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
|
+
# Global registry to store MCP tool session metadata by tool name
|
|
28
|
+
# This is used to pass session info to callbacks since LangChain's serialization doesn't include all fields
|
|
29
|
+
MCP_TOOL_SESSION_REGISTRY: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
|
|
27
31
|
|
|
28
32
|
class McpRemoteTool(McpServerTool):
|
|
29
33
|
"""
|
|
@@ -43,6 +47,7 @@ class McpRemoteTool(McpServerTool):
|
|
|
43
47
|
"""Update metadata with session info after model initialization."""
|
|
44
48
|
super().model_post_init(__context)
|
|
45
49
|
self._update_metadata_with_session()
|
|
50
|
+
self._register_session_metadata()
|
|
46
51
|
|
|
47
52
|
def _update_metadata_with_session(self):
|
|
48
53
|
"""Update the metadata dict with current session information."""
|
|
@@ -54,6 +59,15 @@ class McpRemoteTool(McpServerTool):
|
|
|
54
59
|
'mcp_server_url': canonical_resource(self.server_url)
|
|
55
60
|
})
|
|
56
61
|
|
|
62
|
+
def _register_session_metadata(self):
|
|
63
|
+
"""Register session metadata in global registry for callback access."""
|
|
64
|
+
if self.session_id and self.server_url:
|
|
65
|
+
MCP_TOOL_SESSION_REGISTRY[self.name] = {
|
|
66
|
+
'mcp_session_id': self.session_id,
|
|
67
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
68
|
+
}
|
|
69
|
+
logger.debug(f"[MCP] Registered session metadata for tool '{self.name}': session={self.session_id}")
|
|
70
|
+
|
|
57
71
|
def __getstate__(self):
|
|
58
72
|
"""Custom serialization for pickle compatibility."""
|
|
59
73
|
state = super().__getstate__()
|
|
@@ -85,7 +99,6 @@ class McpRemoteTool(McpServerTool):
|
|
|
85
99
|
|
|
86
100
|
async def _execute_remote_tool(self, kwargs: Dict[str, Any]) -> str:
|
|
87
101
|
"""Execute the actual remote MCP tool call using SSE client."""
|
|
88
|
-
from ...tools.utils import TOOLKIT_SPLITTER
|
|
89
102
|
|
|
90
103
|
# Check for session_id requirement
|
|
91
104
|
if not self.session_id:
|
|
@@ -95,10 +108,10 @@ class McpRemoteTool(McpServerTool):
|
|
|
95
108
|
# Use the original tool name from discovery for MCP server invocation
|
|
96
109
|
tool_name_for_server = self.original_tool_name
|
|
97
110
|
if not tool_name_for_server:
|
|
98
|
-
tool_name_for_server = self.name
|
|
99
|
-
logger.warning(f"original_tool_name not set for '{self.name}', using
|
|
111
|
+
tool_name_for_server = self.name
|
|
112
|
+
logger.warning(f"original_tool_name not set for '{self.name}', using: {tool_name_for_server}")
|
|
100
113
|
|
|
101
|
-
logger.info(f"[MCP
|
|
114
|
+
logger.info(f"[MCP] Executing tool '{tool_name_for_server}' with session {self.session_id}")
|
|
102
115
|
|
|
103
116
|
try:
|
|
104
117
|
# Prepare headers
|
|
@@ -106,16 +119,18 @@ class McpRemoteTool(McpServerTool):
|
|
|
106
119
|
if self.server_headers:
|
|
107
120
|
headers.update(self.server_headers)
|
|
108
121
|
|
|
109
|
-
# Create
|
|
110
|
-
client =
|
|
122
|
+
# Create unified MCP client (auto-detects transport)
|
|
123
|
+
client = McpClient(
|
|
111
124
|
url=self.server_url,
|
|
112
125
|
session_id=self.session_id,
|
|
113
126
|
headers=headers,
|
|
114
127
|
timeout=self.tool_timeout_sec
|
|
115
128
|
)
|
|
116
129
|
|
|
117
|
-
# Execute tool call
|
|
118
|
-
|
|
130
|
+
# Execute tool call (client auto-detects SSE vs Streamable HTTP)
|
|
131
|
+
async with client:
|
|
132
|
+
await client.initialize()
|
|
133
|
+
result = await client.call_tool(tool_name_for_server, kwargs)
|
|
119
134
|
|
|
120
135
|
# Format the result
|
|
121
136
|
if isinstance(result, dict):
|
|
@@ -144,7 +159,7 @@ class McpRemoteTool(McpServerTool):
|
|
|
144
159
|
return str(result)
|
|
145
160
|
|
|
146
161
|
except Exception as e:
|
|
147
|
-
logger.error(f"[MCP
|
|
162
|
+
logger.error(f"[MCP] Tool execution failed: {e}", exc_info=True)
|
|
148
163
|
raise
|
|
149
164
|
|
|
150
165
|
def _parse_sse(self, text: str) -> Dict[str, Any]:
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from logging import getLogger
|
|
3
|
-
from typing import Any, Type, Literal, Optional, Union, List
|
|
3
|
+
from typing import Any, Type, Literal, Optional, Union, List, Annotated
|
|
4
4
|
|
|
5
5
|
from langchain_core.tools import BaseTool
|
|
6
|
-
from pydantic import BaseModel, Field, create_model,
|
|
6
|
+
from pydantic import BaseModel, Field, create_model, ConfigDict, StringConstraints
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# EmailStr moved to pydantic_extra_types in pydantic v2, use str for simplicity
|
|
9
|
+
EmailStr = str
|
|
9
10
|
|
|
10
11
|
logger = getLogger(__name__)
|
|
11
12
|
|
|
@@ -61,7 +62,7 @@ class McpServerTool(BaseTool):
|
|
|
61
62
|
if field.get("format") == "email":
|
|
62
63
|
return EmailStr
|
|
63
64
|
if "pattern" in field:
|
|
64
|
-
return
|
|
65
|
+
return Annotated[str, StringConstraints(pattern=field["pattern"])]
|
|
65
66
|
return str
|
|
66
67
|
if t == "integer":
|
|
67
68
|
return int
|
|
@@ -91,13 +92,13 @@ class McpServerTool(BaseTool):
|
|
|
91
92
|
return create_model(model_name, **fields)
|
|
92
93
|
|
|
93
94
|
def _run(self, *args, **kwargs):
|
|
94
|
-
#
|
|
95
|
+
# Use the tool name directly (no prefix extraction needed)
|
|
95
96
|
call_data = {
|
|
96
97
|
"server": self.server,
|
|
97
98
|
"tool_timeout_sec": self.tool_timeout_sec,
|
|
98
99
|
"tool_call_id": str(uuid.uuid4()),
|
|
99
100
|
"params": {
|
|
100
|
-
"name": self.name
|
|
101
|
+
"name": self.name,
|
|
101
102
|
"arguments": kwargs
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Planning tools for runtime agents.
|
|
3
|
+
|
|
4
|
+
Provides plan management for multi-step task execution with progress tracking.
|
|
5
|
+
Supports two storage backends:
|
|
6
|
+
1. PostgreSQL - when connection_string is provided (production/indexer_worker)
|
|
7
|
+
2. Filesystem - when no connection string (local CLI usage)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .wrapper import (
|
|
11
|
+
PlanningWrapper,
|
|
12
|
+
PlanStep,
|
|
13
|
+
PlanState,
|
|
14
|
+
FilesystemStorage,
|
|
15
|
+
PostgresStorage,
|
|
16
|
+
)
|
|
17
|
+
from .models import (
|
|
18
|
+
AgentPlan,
|
|
19
|
+
PlanStatus,
|
|
20
|
+
ensure_plan_tables,
|
|
21
|
+
delete_plan_by_conversation_id,
|
|
22
|
+
cleanup_on_graceful_completion
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"PlanningWrapper",
|
|
27
|
+
"PlanStep",
|
|
28
|
+
"PlanState",
|
|
29
|
+
"FilesystemStorage",
|
|
30
|
+
"PostgresStorage",
|
|
31
|
+
"AgentPlan",
|
|
32
|
+
"PlanStatus",
|
|
33
|
+
"ensure_plan_tables",
|
|
34
|
+
"delete_plan_by_conversation_id",
|
|
35
|
+
"cleanup_on_graceful_completion",
|
|
36
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLAlchemy models for agent planning.
|
|
3
|
+
|
|
4
|
+
Defines the AgentPlan table for storing execution plans with steps.
|
|
5
|
+
Table is created automatically on toolkit initialization if it doesn't exist.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import List, Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
from sqlalchemy import Column, String, DateTime, Text, Index, text
|
|
16
|
+
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
|
17
|
+
from sqlalchemy.orm import declarative_base
|
|
18
|
+
from sqlalchemy import create_engine
|
|
19
|
+
from sqlalchemy.exc import ProgrammingError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
Base = declarative_base()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PlanStatus(str, enum.Enum):
|
|
27
|
+
"""Status of an execution plan."""
|
|
28
|
+
in_progress = "in_progress"
|
|
29
|
+
completed = "completed"
|
|
30
|
+
abandoned = "abandoned"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentPlan(Base):
|
|
34
|
+
"""
|
|
35
|
+
Stores execution plans for agent tasks.
|
|
36
|
+
|
|
37
|
+
Created in the project-specific pgvector database.
|
|
38
|
+
Plans are scoped by conversation_id (from server or CLI session_id).
|
|
39
|
+
"""
|
|
40
|
+
__tablename__ = "agent_plans"
|
|
41
|
+
|
|
42
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
43
|
+
conversation_id = Column(String(255), nullable=False, index=True)
|
|
44
|
+
|
|
45
|
+
# Plan metadata
|
|
46
|
+
title = Column(String(255), nullable=True)
|
|
47
|
+
status = Column(String(50), default=PlanStatus.in_progress.value)
|
|
48
|
+
|
|
49
|
+
# Plan content (JSONB for flexible step storage)
|
|
50
|
+
# Structure: {"steps": [{"description": "...", "completed": false}, ...]}
|
|
51
|
+
plan_data = Column(JSONB, nullable=False, default=dict)
|
|
52
|
+
|
|
53
|
+
# Timestamps
|
|
54
|
+
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
|
55
|
+
updated_at = Column(DateTime, nullable=True, onupdate=datetime.utcnow)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Pydantic models for tool input/output
|
|
59
|
+
class PlanStep(BaseModel):
|
|
60
|
+
"""A single step in a plan."""
|
|
61
|
+
description: str = Field(description="Step description")
|
|
62
|
+
completed: bool = Field(default=False, description="Whether step is completed")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PlanState(BaseModel):
|
|
66
|
+
"""Current plan state for serialization."""
|
|
67
|
+
title: str = Field(default="", description="Plan title")
|
|
68
|
+
steps: List[PlanStep] = Field(default_factory=list, description="List of steps")
|
|
69
|
+
status: str = Field(default=PlanStatus.in_progress.value, description="Plan status")
|
|
70
|
+
|
|
71
|
+
def render(self) -> str:
|
|
72
|
+
"""Render plan as formatted string with checkboxes."""
|
|
73
|
+
if not self.steps:
|
|
74
|
+
return "No plan currently set."
|
|
75
|
+
|
|
76
|
+
lines = []
|
|
77
|
+
if self.title:
|
|
78
|
+
lines.append(f"📋 {self.title}")
|
|
79
|
+
|
|
80
|
+
completed_count = 0
|
|
81
|
+
for i, step in enumerate(self.steps, 1):
|
|
82
|
+
checkbox = "☑" if step.completed else "☐"
|
|
83
|
+
status_text = " (completed)" if step.completed else ""
|
|
84
|
+
lines.append(f" {checkbox} {i}. {step.description}{status_text}")
|
|
85
|
+
if step.completed:
|
|
86
|
+
completed_count += 1
|
|
87
|
+
|
|
88
|
+
lines.append(f"\nProgress: {completed_count}/{len(self.steps)} steps completed")
|
|
89
|
+
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
93
|
+
"""Convert to dictionary for JSONB storage."""
|
|
94
|
+
return {
|
|
95
|
+
"steps": [{"description": s.description, "completed": s.completed} for s in self.steps]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: Dict[str, Any], title: str = "", status: str = PlanStatus.in_progress.value) -> "PlanState":
|
|
100
|
+
"""Create from dictionary (JSONB data)."""
|
|
101
|
+
steps_data = data.get("steps", [])
|
|
102
|
+
steps = [PlanStep(**s) if isinstance(s, dict) else s for s in steps_data]
|
|
103
|
+
return cls(title=title, steps=steps, status=status)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ensure_plan_tables(connection_string: str) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Ensure the agent_plans table exists in the database.
|
|
109
|
+
|
|
110
|
+
Creates the table if it doesn't exist. Safe to call multiple times.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
connection_string: PostgreSQL connection string
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if table was created or already exists, False on error
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Handle SecretStr if passed
|
|
120
|
+
if hasattr(connection_string, 'get_secret_value'):
|
|
121
|
+
connection_string = connection_string.get_secret_value()
|
|
122
|
+
|
|
123
|
+
if not connection_string:
|
|
124
|
+
logger.warning("No connection string provided for plan tables")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
engine = create_engine(connection_string)
|
|
128
|
+
|
|
129
|
+
# Create tables if they don't exist
|
|
130
|
+
Base.metadata.create_all(engine, checkfirst=True)
|
|
131
|
+
|
|
132
|
+
logger.debug("Agent plans table ensured")
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Failed to ensure plan tables: {e}")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def delete_plan_by_conversation_id(connection_string: str, conversation_id: str) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Delete a plan by conversation_id.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
connection_string: PostgreSQL connection string
|
|
146
|
+
conversation_id: The conversation ID to delete plans for
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if deletion successful, False otherwise
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
if hasattr(connection_string, 'get_secret_value'):
|
|
153
|
+
connection_string = connection_string.get_secret_value()
|
|
154
|
+
|
|
155
|
+
if not connection_string or not conversation_id:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
engine = create_engine(connection_string)
|
|
159
|
+
|
|
160
|
+
with engine.connect() as conn:
|
|
161
|
+
result = conn.execute(
|
|
162
|
+
text("DELETE FROM agent_plans WHERE conversation_id = :conversation_id"),
|
|
163
|
+
{"conversation_id": conversation_id}
|
|
164
|
+
)
|
|
165
|
+
conn.commit()
|
|
166
|
+
|
|
167
|
+
logger.debug(f"Deleted plan for conversation_id: {conversation_id}")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Failed to delete plan for conversation_id {conversation_id}: {e}")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cleanup_on_graceful_completion(
|
|
176
|
+
connection_string: str,
|
|
177
|
+
conversation_id: str,
|
|
178
|
+
thread_id: str = None,
|
|
179
|
+
delete_checkpoints: bool = True
|
|
180
|
+
) -> dict:
|
|
181
|
+
"""
|
|
182
|
+
Cleanup plans and optionally checkpoints after graceful agent completion.
|
|
183
|
+
|
|
184
|
+
This function is designed to be called after an agent completes successfully
|
|
185
|
+
(no exceptions, valid finish reason).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
connection_string: PostgreSQL connection string
|
|
189
|
+
conversation_id: The conversation ID to cleanup plans for
|
|
190
|
+
thread_id: The thread ID to cleanup checkpoints for (optional)
|
|
191
|
+
delete_checkpoints: If True, also delete checkpoint data
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dict with cleanup results: {'plan_deleted': bool, 'checkpoints_deleted': bool}
|
|
195
|
+
"""
|
|
196
|
+
result = {'plan_deleted': False, 'checkpoints_deleted': False}
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
if hasattr(connection_string, 'get_secret_value'):
|
|
200
|
+
connection_string = connection_string.get_secret_value()
|
|
201
|
+
|
|
202
|
+
if not connection_string or not conversation_id:
|
|
203
|
+
logger.warning("Missing connection_string or conversation_id for cleanup")
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
engine = create_engine(connection_string)
|
|
207
|
+
|
|
208
|
+
with engine.connect() as conn:
|
|
209
|
+
# Delete plan by conversation_id
|
|
210
|
+
try:
|
|
211
|
+
conn.execute(
|
|
212
|
+
text("DELETE FROM agent_plans WHERE conversation_id = :conversation_id"),
|
|
213
|
+
{"conversation_id": conversation_id}
|
|
214
|
+
)
|
|
215
|
+
result['plan_deleted'] = True
|
|
216
|
+
logger.debug(f"Deleted plan for conversation_id: {conversation_id}")
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# Table might not exist, which is fine
|
|
219
|
+
logger.debug(f"Could not delete plan (table may not exist): {e}")
|
|
220
|
+
|
|
221
|
+
# Delete checkpoints if requested (still uses thread_id as that's LangGraph's key)
|
|
222
|
+
if delete_checkpoints and thread_id:
|
|
223
|
+
checkpoint_tables = [
|
|
224
|
+
"checkpoints",
|
|
225
|
+
"checkpoint_writes",
|
|
226
|
+
"checkpoint_blobs"
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
for table in checkpoint_tables:
|
|
230
|
+
try:
|
|
231
|
+
conn.execute(
|
|
232
|
+
text(f"DELETE FROM {table} WHERE thread_id = :thread_id"),
|
|
233
|
+
{"thread_id": thread_id}
|
|
234
|
+
)
|
|
235
|
+
logger.debug(f"Deleted {table} for thread_id: {thread_id}")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.debug(f"Could not delete from {table}: {e}")
|
|
238
|
+
|
|
239
|
+
result['checkpoints_deleted'] = True
|
|
240
|
+
|
|
241
|
+
conn.commit()
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Failed to cleanup for conversation_id {conversation_id}: {e}")
|
|
245
|
+
|
|
246
|
+
return result
|