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.
Files changed (261) hide show
  1. alita_sdk/cli/agent/__init__.py +5 -0
  2. alita_sdk/cli/agent/default.py +258 -0
  3. alita_sdk/cli/agent_executor.py +15 -3
  4. alita_sdk/cli/agent_loader.py +56 -8
  5. alita_sdk/cli/agent_ui.py +93 -31
  6. alita_sdk/cli/agents.py +2274 -230
  7. alita_sdk/cli/callbacks.py +96 -25
  8. alita_sdk/cli/cli.py +10 -1
  9. alita_sdk/cli/config.py +162 -9
  10. alita_sdk/cli/context/__init__.py +30 -0
  11. alita_sdk/cli/context/cleanup.py +198 -0
  12. alita_sdk/cli/context/manager.py +731 -0
  13. alita_sdk/cli/context/message.py +285 -0
  14. alita_sdk/cli/context/strategies.py +289 -0
  15. alita_sdk/cli/context/token_estimation.py +127 -0
  16. alita_sdk/cli/input_handler.py +419 -0
  17. alita_sdk/cli/inventory.py +1073 -0
  18. alita_sdk/cli/testcases/__init__.py +94 -0
  19. alita_sdk/cli/testcases/data_generation.py +119 -0
  20. alita_sdk/cli/testcases/discovery.py +96 -0
  21. alita_sdk/cli/testcases/executor.py +84 -0
  22. alita_sdk/cli/testcases/logger.py +85 -0
  23. alita_sdk/cli/testcases/parser.py +172 -0
  24. alita_sdk/cli/testcases/prompts.py +91 -0
  25. alita_sdk/cli/testcases/reporting.py +125 -0
  26. alita_sdk/cli/testcases/setup.py +108 -0
  27. alita_sdk/cli/testcases/test_runner.py +282 -0
  28. alita_sdk/cli/testcases/utils.py +39 -0
  29. alita_sdk/cli/testcases/validation.py +90 -0
  30. alita_sdk/cli/testcases/workflow.py +196 -0
  31. alita_sdk/cli/toolkit.py +14 -17
  32. alita_sdk/cli/toolkit_loader.py +35 -5
  33. alita_sdk/cli/tools/__init__.py +36 -2
  34. alita_sdk/cli/tools/approval.py +224 -0
  35. alita_sdk/cli/tools/filesystem.py +910 -64
  36. alita_sdk/cli/tools/planning.py +389 -0
  37. alita_sdk/cli/tools/terminal.py +414 -0
  38. alita_sdk/community/__init__.py +72 -12
  39. alita_sdk/community/inventory/__init__.py +236 -0
  40. alita_sdk/community/inventory/config.py +257 -0
  41. alita_sdk/community/inventory/enrichment.py +2137 -0
  42. alita_sdk/community/inventory/extractors.py +1469 -0
  43. alita_sdk/community/inventory/ingestion.py +3172 -0
  44. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  45. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  46. alita_sdk/community/inventory/parsers/base.py +295 -0
  47. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  48. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  49. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  50. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  51. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  52. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  53. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  54. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  55. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  56. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  57. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  58. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  59. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  60. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  61. alita_sdk/community/inventory/patterns/loader.py +348 -0
  62. alita_sdk/community/inventory/patterns/registry.py +198 -0
  63. alita_sdk/community/inventory/presets.py +535 -0
  64. alita_sdk/community/inventory/retrieval.py +1403 -0
  65. alita_sdk/community/inventory/toolkit.py +173 -0
  66. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  67. alita_sdk/community/inventory/visualize.py +1370 -0
  68. alita_sdk/configurations/__init__.py +1 -1
  69. alita_sdk/configurations/ado.py +141 -20
  70. alita_sdk/configurations/bitbucket.py +0 -3
  71. alita_sdk/configurations/confluence.py +76 -42
  72. alita_sdk/configurations/figma.py +76 -0
  73. alita_sdk/configurations/gitlab.py +17 -5
  74. alita_sdk/configurations/openapi.py +329 -0
  75. alita_sdk/configurations/qtest.py +72 -1
  76. alita_sdk/configurations/report_portal.py +96 -0
  77. alita_sdk/configurations/sharepoint.py +148 -0
  78. alita_sdk/configurations/testio.py +83 -0
  79. alita_sdk/runtime/clients/artifact.py +3 -3
  80. alita_sdk/runtime/clients/client.py +353 -48
  81. alita_sdk/runtime/clients/sandbox_client.py +0 -21
  82. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  83. alita_sdk/runtime/langchain/assistant.py +123 -26
  84. alita_sdk/runtime/langchain/constants.py +642 -1
  85. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  86. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  87. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
  88. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
  89. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  90. alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
  91. alita_sdk/runtime/langchain/langraph_agent.py +279 -73
  92. alita_sdk/runtime/langchain/utils.py +82 -15
  93. alita_sdk/runtime/llms/preloaded.py +2 -6
  94. alita_sdk/runtime/skills/__init__.py +91 -0
  95. alita_sdk/runtime/skills/callbacks.py +498 -0
  96. alita_sdk/runtime/skills/discovery.py +540 -0
  97. alita_sdk/runtime/skills/executor.py +610 -0
  98. alita_sdk/runtime/skills/input_builder.py +371 -0
  99. alita_sdk/runtime/skills/models.py +330 -0
  100. alita_sdk/runtime/skills/registry.py +355 -0
  101. alita_sdk/runtime/skills/skill_runner.py +330 -0
  102. alita_sdk/runtime/toolkits/__init__.py +7 -0
  103. alita_sdk/runtime/toolkits/application.py +21 -9
  104. alita_sdk/runtime/toolkits/artifact.py +15 -5
  105. alita_sdk/runtime/toolkits/datasource.py +13 -6
  106. alita_sdk/runtime/toolkits/mcp.py +139 -251
  107. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  108. alita_sdk/runtime/toolkits/planning.py +178 -0
  109. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  110. alita_sdk/runtime/toolkits/subgraph.py +251 -6
  111. alita_sdk/runtime/toolkits/tools.py +238 -32
  112. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  113. alita_sdk/runtime/tools/__init__.py +3 -1
  114. alita_sdk/runtime/tools/application.py +20 -6
  115. alita_sdk/runtime/tools/artifact.py +511 -28
  116. alita_sdk/runtime/tools/data_analysis.py +183 -0
  117. alita_sdk/runtime/tools/function.py +43 -15
  118. alita_sdk/runtime/tools/image_generation.py +50 -44
  119. alita_sdk/runtime/tools/llm.py +852 -67
  120. alita_sdk/runtime/tools/loop.py +3 -1
  121. alita_sdk/runtime/tools/loop_output.py +3 -1
  122. alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
  123. alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
  124. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  125. alita_sdk/runtime/tools/planning/models.py +246 -0
  126. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  127. alita_sdk/runtime/tools/router.py +2 -4
  128. alita_sdk/runtime/tools/sandbox.py +9 -6
  129. alita_sdk/runtime/tools/skill_router.py +776 -0
  130. alita_sdk/runtime/tools/tool.py +3 -1
  131. alita_sdk/runtime/tools/vectorstore.py +7 -2
  132. alita_sdk/runtime/tools/vectorstore_base.py +51 -11
  133. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  134. alita_sdk/runtime/utils/constants.py +5 -1
  135. alita_sdk/runtime/utils/mcp_client.py +492 -0
  136. alita_sdk/runtime/utils/mcp_oauth.py +202 -5
  137. alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
  138. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  139. alita_sdk/runtime/utils/serialization.py +155 -0
  140. alita_sdk/runtime/utils/streamlit.py +6 -10
  141. alita_sdk/runtime/utils/toolkit_utils.py +16 -5
  142. alita_sdk/runtime/utils/utils.py +36 -0
  143. alita_sdk/tools/__init__.py +113 -29
  144. alita_sdk/tools/ado/repos/__init__.py +51 -33
  145. alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
  146. alita_sdk/tools/ado/test_plan/__init__.py +25 -9
  147. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  148. alita_sdk/tools/ado/utils.py +1 -18
  149. alita_sdk/tools/ado/wiki/__init__.py +25 -8
  150. alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
  151. alita_sdk/tools/ado/work_item/__init__.py +26 -9
  152. alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
  153. alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
  154. alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
  155. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  156. alita_sdk/tools/azure_ai/search/__init__.py +11 -8
  157. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  158. alita_sdk/tools/base/tool.py +5 -1
  159. alita_sdk/tools/base_indexer_toolkit.py +170 -45
  160. alita_sdk/tools/bitbucket/__init__.py +17 -12
  161. alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
  162. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
  163. alita_sdk/tools/browser/__init__.py +5 -4
  164. alita_sdk/tools/carrier/__init__.py +5 -6
  165. alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
  166. alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
  167. alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
  168. alita_sdk/tools/chunkers/__init__.py +3 -1
  169. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  170. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  171. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  172. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  173. alita_sdk/tools/cloud/aws/__init__.py +10 -7
  174. alita_sdk/tools/cloud/azure/__init__.py +10 -7
  175. alita_sdk/tools/cloud/gcp/__init__.py +10 -7
  176. alita_sdk/tools/cloud/k8s/__init__.py +10 -7
  177. alita_sdk/tools/code/linter/__init__.py +10 -8
  178. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  179. alita_sdk/tools/code/sonar/__init__.py +10 -7
  180. alita_sdk/tools/code_indexer_toolkit.py +73 -23
  181. alita_sdk/tools/confluence/__init__.py +21 -15
  182. alita_sdk/tools/confluence/api_wrapper.py +78 -23
  183. alita_sdk/tools/confluence/loader.py +4 -2
  184. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  185. alita_sdk/tools/elastic/__init__.py +11 -8
  186. alita_sdk/tools/elitea_base.py +493 -30
  187. alita_sdk/tools/figma/__init__.py +58 -11
  188. alita_sdk/tools/figma/api_wrapper.py +1235 -143
  189. alita_sdk/tools/figma/figma_client.py +73 -0
  190. alita_sdk/tools/figma/toon_tools.py +2748 -0
  191. alita_sdk/tools/github/__init__.py +13 -14
  192. alita_sdk/tools/github/github_client.py +224 -100
  193. alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
  194. alita_sdk/tools/github/schemas.py +14 -5
  195. alita_sdk/tools/github/tool.py +5 -1
  196. alita_sdk/tools/github/tool_prompts.py +9 -22
  197. alita_sdk/tools/gitlab/__init__.py +15 -11
  198. alita_sdk/tools/gitlab/api_wrapper.py +207 -41
  199. alita_sdk/tools/gitlab_org/__init__.py +10 -8
  200. alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
  201. alita_sdk/tools/google/bigquery/__init__.py +13 -12
  202. alita_sdk/tools/google/bigquery/tool.py +5 -1
  203. alita_sdk/tools/google_places/__init__.py +10 -8
  204. alita_sdk/tools/google_places/api_wrapper.py +1 -1
  205. alita_sdk/tools/jira/__init__.py +17 -11
  206. alita_sdk/tools/jira/api_wrapper.py +91 -40
  207. alita_sdk/tools/keycloak/__init__.py +11 -8
  208. alita_sdk/tools/localgit/__init__.py +9 -3
  209. alita_sdk/tools/localgit/local_git.py +62 -54
  210. alita_sdk/tools/localgit/tool.py +5 -1
  211. alita_sdk/tools/memory/__init__.py +11 -3
  212. alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
  213. alita_sdk/tools/ocr/__init__.py +11 -8
  214. alita_sdk/tools/openapi/__init__.py +490 -114
  215. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  216. alita_sdk/tools/openapi/tool.py +20 -0
  217. alita_sdk/tools/pandas/__init__.py +20 -12
  218. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  219. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  220. alita_sdk/tools/postman/__init__.py +11 -11
  221. alita_sdk/tools/pptx/__init__.py +10 -9
  222. alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
  223. alita_sdk/tools/qtest/__init__.py +30 -10
  224. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  225. alita_sdk/tools/rally/__init__.py +10 -8
  226. alita_sdk/tools/rally/api_wrapper.py +1 -1
  227. alita_sdk/tools/report_portal/__init__.py +12 -9
  228. alita_sdk/tools/salesforce/__init__.py +10 -9
  229. alita_sdk/tools/servicenow/__init__.py +17 -14
  230. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  231. alita_sdk/tools/sharepoint/__init__.py +10 -8
  232. alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
  233. alita_sdk/tools/slack/__init__.py +10 -8
  234. alita_sdk/tools/slack/api_wrapper.py +2 -2
  235. alita_sdk/tools/sql/__init__.py +11 -9
  236. alita_sdk/tools/testio/__init__.py +10 -8
  237. alita_sdk/tools/testrail/__init__.py +11 -8
  238. alita_sdk/tools/testrail/api_wrapper.py +1 -1
  239. alita_sdk/tools/utils/__init__.py +9 -4
  240. alita_sdk/tools/utils/content_parser.py +77 -3
  241. alita_sdk/tools/utils/text_operations.py +410 -0
  242. alita_sdk/tools/utils/tool_prompts.py +79 -0
  243. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
  244. alita_sdk/tools/xray/__init__.py +12 -9
  245. alita_sdk/tools/yagmail/__init__.py +9 -3
  246. alita_sdk/tools/zephyr/__init__.py +9 -7
  247. alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
  248. alita_sdk/tools/zephyr_essential/__init__.py +10 -8
  249. alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
  250. alita_sdk/tools/zephyr_essential/client.py +2 -2
  251. alita_sdk/tools/zephyr_scale/__init__.py +11 -9
  252. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  253. alita_sdk/tools/zephyr_squad/__init__.py +10 -8
  254. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
  255. alita_sdk-0.3.627.dist-info/RECORD +468 -0
  256. alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
  257. alita_sdk-0.3.462.dist-info/RECORD +0 -384
  258. alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
  259. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
  260. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
  261. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
@@ -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
- loop_data = _old_extract_json(completion.content.strip())
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
- result = _extract_json(completion.content.strip())
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.mcp_sse_client import McpSseClient
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.rsplit(TOOLKIT_SPLITTER, 1)[-1] if TOOLKIT_SPLITTER in self.name else self.name
99
- logger.warning(f"original_tool_name not set for '{self.name}', using extracted: {tool_name_for_server}")
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 SSE] Executing tool '{tool_name_for_server}' with session {self.session_id}")
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 SSE client
110
- client = McpSseClient(
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 via SSE
118
- result = await client.call_tool(tool_name_for_server, kwargs)
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 SSE] Tool execution failed: {e}", exc_info=True)
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, EmailStr, constr, ConfigDict
6
+ from pydantic import BaseModel, Field, create_model, ConfigDict, StringConstraints
7
7
 
8
- from ...tools.utils import TOOLKIT_SPLITTER
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 constr(regex=field["pattern"])
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
- # Extract the actual tool/prompt name (remove toolkit prefix)
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.rsplit(TOOLKIT_SPLITTER)[1] if TOOLKIT_SPLITTER in self.name else 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