alita-sdk 0.3.257__py3-none-any.whl → 0.3.562__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 (278) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +155 -0
  6. alita_sdk/cli/agent_loader.py +215 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3601 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/toolkit.py +327 -0
  23. alita_sdk/cli/toolkit_loader.py +85 -0
  24. alita_sdk/cli/tools/__init__.py +43 -0
  25. alita_sdk/cli/tools/approval.py +224 -0
  26. alita_sdk/cli/tools/filesystem.py +1751 -0
  27. alita_sdk/cli/tools/planning.py +389 -0
  28. alita_sdk/cli/tools/terminal.py +414 -0
  29. alita_sdk/community/__init__.py +72 -12
  30. alita_sdk/community/inventory/__init__.py +236 -0
  31. alita_sdk/community/inventory/config.py +257 -0
  32. alita_sdk/community/inventory/enrichment.py +2137 -0
  33. alita_sdk/community/inventory/extractors.py +1469 -0
  34. alita_sdk/community/inventory/ingestion.py +3172 -0
  35. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  36. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  37. alita_sdk/community/inventory/parsers/base.py +295 -0
  38. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  39. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  40. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  41. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  42. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  43. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  44. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  45. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  46. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  47. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  48. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  49. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  50. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  51. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  52. alita_sdk/community/inventory/patterns/loader.py +348 -0
  53. alita_sdk/community/inventory/patterns/registry.py +198 -0
  54. alita_sdk/community/inventory/presets.py +535 -0
  55. alita_sdk/community/inventory/retrieval.py +1403 -0
  56. alita_sdk/community/inventory/toolkit.py +173 -0
  57. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  58. alita_sdk/community/inventory/visualize.py +1370 -0
  59. alita_sdk/configurations/__init__.py +11 -0
  60. alita_sdk/configurations/ado.py +148 -2
  61. alita_sdk/configurations/azure_search.py +1 -1
  62. alita_sdk/configurations/bigquery.py +1 -1
  63. alita_sdk/configurations/bitbucket.py +94 -2
  64. alita_sdk/configurations/browser.py +18 -0
  65. alita_sdk/configurations/carrier.py +19 -0
  66. alita_sdk/configurations/confluence.py +130 -1
  67. alita_sdk/configurations/delta_lake.py +1 -1
  68. alita_sdk/configurations/figma.py +76 -5
  69. alita_sdk/configurations/github.py +65 -1
  70. alita_sdk/configurations/gitlab.py +81 -0
  71. alita_sdk/configurations/google_places.py +17 -0
  72. alita_sdk/configurations/jira.py +103 -0
  73. alita_sdk/configurations/openapi.py +111 -0
  74. alita_sdk/configurations/postman.py +1 -1
  75. alita_sdk/configurations/qtest.py +72 -3
  76. alita_sdk/configurations/report_portal.py +115 -0
  77. alita_sdk/configurations/salesforce.py +19 -0
  78. alita_sdk/configurations/service_now.py +1 -12
  79. alita_sdk/configurations/sharepoint.py +167 -0
  80. alita_sdk/configurations/sonar.py +18 -0
  81. alita_sdk/configurations/sql.py +20 -0
  82. alita_sdk/configurations/testio.py +101 -0
  83. alita_sdk/configurations/testrail.py +88 -0
  84. alita_sdk/configurations/xray.py +94 -1
  85. alita_sdk/configurations/zephyr_enterprise.py +94 -1
  86. alita_sdk/configurations/zephyr_essential.py +95 -0
  87. alita_sdk/runtime/clients/artifact.py +21 -4
  88. alita_sdk/runtime/clients/client.py +458 -67
  89. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  90. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  91. alita_sdk/runtime/clients/sandbox_client.py +352 -0
  92. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  93. alita_sdk/runtime/langchain/assistant.py +183 -43
  94. alita_sdk/runtime/langchain/constants.py +647 -1
  95. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  96. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
  97. alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
  100. alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
  101. alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
  102. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
  103. alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
  104. alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
  105. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
  106. alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
  107. alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
  108. alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
  109. alita_sdk/runtime/langchain/langraph_agent.py +407 -92
  110. alita_sdk/runtime/langchain/utils.py +102 -8
  111. alita_sdk/runtime/llms/preloaded.py +2 -6
  112. alita_sdk/runtime/models/mcp_models.py +61 -0
  113. alita_sdk/runtime/skills/__init__.py +91 -0
  114. alita_sdk/runtime/skills/callbacks.py +498 -0
  115. alita_sdk/runtime/skills/discovery.py +540 -0
  116. alita_sdk/runtime/skills/executor.py +610 -0
  117. alita_sdk/runtime/skills/input_builder.py +371 -0
  118. alita_sdk/runtime/skills/models.py +330 -0
  119. alita_sdk/runtime/skills/registry.py +355 -0
  120. alita_sdk/runtime/skills/skill_runner.py +330 -0
  121. alita_sdk/runtime/toolkits/__init__.py +28 -0
  122. alita_sdk/runtime/toolkits/application.py +14 -4
  123. alita_sdk/runtime/toolkits/artifact.py +24 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +780 -0
  126. alita_sdk/runtime/toolkits/planning.py +178 -0
  127. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  128. alita_sdk/runtime/toolkits/subgraph.py +11 -6
  129. alita_sdk/runtime/toolkits/tools.py +314 -70
  130. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  131. alita_sdk/runtime/tools/__init__.py +24 -0
  132. alita_sdk/runtime/tools/application.py +16 -4
  133. alita_sdk/runtime/tools/artifact.py +367 -33
  134. alita_sdk/runtime/tools/data_analysis.py +183 -0
  135. alita_sdk/runtime/tools/function.py +100 -4
  136. alita_sdk/runtime/tools/graph.py +81 -0
  137. alita_sdk/runtime/tools/image_generation.py +218 -0
  138. alita_sdk/runtime/tools/llm.py +1013 -177
  139. alita_sdk/runtime/tools/loop.py +3 -1
  140. alita_sdk/runtime/tools/loop_output.py +3 -1
  141. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  142. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  143. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  144. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  145. alita_sdk/runtime/tools/planning/models.py +246 -0
  146. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  147. alita_sdk/runtime/tools/router.py +2 -1
  148. alita_sdk/runtime/tools/sandbox.py +375 -0
  149. alita_sdk/runtime/tools/skill_router.py +776 -0
  150. alita_sdk/runtime/tools/tool.py +3 -1
  151. alita_sdk/runtime/tools/vectorstore.py +69 -65
  152. alita_sdk/runtime/tools/vectorstore_base.py +163 -90
  153. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  154. alita_sdk/runtime/utils/mcp_client.py +492 -0
  155. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  156. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  157. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  158. alita_sdk/runtime/utils/streamlit.py +41 -14
  159. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  160. alita_sdk/runtime/utils/utils.py +48 -0
  161. alita_sdk/tools/__init__.py +135 -37
  162. alita_sdk/tools/ado/__init__.py +2 -2
  163. alita_sdk/tools/ado/repos/__init__.py +15 -19
  164. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  165. alita_sdk/tools/ado/test_plan/__init__.py +26 -8
  166. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  167. alita_sdk/tools/ado/wiki/__init__.py +27 -12
  168. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  169. alita_sdk/tools/ado/work_item/__init__.py +27 -12
  170. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  171. alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
  172. alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
  173. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  174. alita_sdk/tools/azure_ai/search/__init__.py +13 -8
  175. alita_sdk/tools/base/tool.py +5 -1
  176. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  177. alita_sdk/tools/bitbucket/__init__.py +27 -19
  178. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  179. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  180. alita_sdk/tools/browser/__init__.py +41 -16
  181. alita_sdk/tools/browser/crawler.py +3 -1
  182. alita_sdk/tools/browser/utils.py +15 -6
  183. alita_sdk/tools/carrier/__init__.py +18 -17
  184. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  185. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  186. alita_sdk/tools/chunkers/__init__.py +3 -1
  187. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  188. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  189. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  190. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  191. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  192. alita_sdk/tools/cloud/aws/__init__.py +11 -7
  193. alita_sdk/tools/cloud/azure/__init__.py +11 -7
  194. alita_sdk/tools/cloud/gcp/__init__.py +11 -7
  195. alita_sdk/tools/cloud/k8s/__init__.py +11 -7
  196. alita_sdk/tools/code/linter/__init__.py +9 -8
  197. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  198. alita_sdk/tools/code/sonar/__init__.py +20 -13
  199. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  200. alita_sdk/tools/confluence/__init__.py +21 -14
  201. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  202. alita_sdk/tools/confluence/loader.py +14 -2
  203. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  204. alita_sdk/tools/elastic/__init__.py +10 -8
  205. alita_sdk/tools/elitea_base.py +546 -64
  206. alita_sdk/tools/figma/__init__.py +11 -8
  207. alita_sdk/tools/figma/api_wrapper.py +352 -153
  208. alita_sdk/tools/github/__init__.py +17 -17
  209. alita_sdk/tools/github/api_wrapper.py +9 -26
  210. alita_sdk/tools/github/github_client.py +81 -12
  211. alita_sdk/tools/github/schemas.py +2 -1
  212. alita_sdk/tools/github/tool.py +5 -1
  213. alita_sdk/tools/gitlab/__init__.py +18 -13
  214. alita_sdk/tools/gitlab/api_wrapper.py +224 -80
  215. alita_sdk/tools/gitlab_org/__init__.py +13 -10
  216. alita_sdk/tools/google/bigquery/__init__.py +13 -13
  217. alita_sdk/tools/google/bigquery/tool.py +5 -1
  218. alita_sdk/tools/google_places/__init__.py +20 -11
  219. alita_sdk/tools/jira/__init__.py +21 -11
  220. alita_sdk/tools/jira/api_wrapper.py +315 -168
  221. alita_sdk/tools/keycloak/__init__.py +10 -8
  222. alita_sdk/tools/localgit/__init__.py +8 -3
  223. alita_sdk/tools/localgit/local_git.py +62 -54
  224. alita_sdk/tools/localgit/tool.py +5 -1
  225. alita_sdk/tools/memory/__init__.py +38 -14
  226. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  227. alita_sdk/tools/ocr/__init__.py +10 -8
  228. alita_sdk/tools/openapi/__init__.py +281 -108
  229. alita_sdk/tools/openapi/api_wrapper.py +883 -0
  230. alita_sdk/tools/openapi/tool.py +20 -0
  231. alita_sdk/tools/pandas/__init__.py +18 -11
  232. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  233. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  234. alita_sdk/tools/postman/__init__.py +10 -11
  235. alita_sdk/tools/postman/api_wrapper.py +19 -8
  236. alita_sdk/tools/postman/postman_analysis.py +8 -1
  237. alita_sdk/tools/pptx/__init__.py +10 -10
  238. alita_sdk/tools/qtest/__init__.py +21 -14
  239. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  240. alita_sdk/tools/rally/__init__.py +12 -10
  241. alita_sdk/tools/report_portal/__init__.py +22 -16
  242. alita_sdk/tools/salesforce/__init__.py +21 -16
  243. alita_sdk/tools/servicenow/__init__.py +20 -16
  244. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  245. alita_sdk/tools/sharepoint/__init__.py +16 -14
  246. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  247. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  248. alita_sdk/tools/sharepoint/utils.py +8 -2
  249. alita_sdk/tools/slack/__init__.py +11 -7
  250. alita_sdk/tools/sql/__init__.py +21 -19
  251. alita_sdk/tools/sql/api_wrapper.py +71 -23
  252. alita_sdk/tools/testio/__init__.py +20 -13
  253. alita_sdk/tools/testrail/__init__.py +12 -11
  254. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  255. alita_sdk/tools/utils/__init__.py +28 -4
  256. alita_sdk/tools/utils/content_parser.py +182 -62
  257. alita_sdk/tools/utils/text_operations.py +254 -0
  258. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  259. alita_sdk/tools/xray/__init__.py +17 -14
  260. alita_sdk/tools/xray/api_wrapper.py +58 -113
  261. alita_sdk/tools/yagmail/__init__.py +8 -3
  262. alita_sdk/tools/zephyr/__init__.py +11 -7
  263. alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
  264. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  265. alita_sdk/tools/zephyr_essential/__init__.py +15 -10
  266. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  267. alita_sdk/tools/zephyr_essential/client.py +6 -4
  268. alita_sdk/tools/zephyr_scale/__init__.py +12 -8
  269. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  270. alita_sdk/tools/zephyr_squad/__init__.py +11 -7
  271. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
  272. alita_sdk-0.3.562.dist-info/RECORD +450 -0
  273. alita_sdk-0.3.562.dist-info/entry_points.txt +2 -0
  274. alita_sdk/tools/bitbucket/tools.py +0 -304
  275. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  276. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/WHEEL +0 -0
  277. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
  278. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,607 @@
1
+ """
2
+ PlanningWrapper - Adaptive API wrapper for plan CRUD operations.
3
+
4
+ Supports two storage backends:
5
+ 1. PostgreSQL - when connection_string is provided (production/indexer_worker)
6
+ 2. Filesystem - when no connection string (local CLI usage)
7
+
8
+ Plans are scoped by conversation_id (from server) or session_id (from CLI).
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import logging
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import List, Dict, Any, Optional
17
+ from pydantic import BaseModel, Field, model_validator
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class PlanStep(BaseModel):
23
+ """A single step in a plan."""
24
+ description: str = Field(description="Step description")
25
+ completed: bool = Field(default=False, description="Whether step is completed")
26
+
27
+
28
+ class PlanState(BaseModel):
29
+ """Current plan state."""
30
+ title: str = Field(default="", description="Plan title")
31
+ steps: List[PlanStep] = Field(default_factory=list, description="List of steps")
32
+ status: str = Field(default="in_progress", description="Plan status")
33
+ conversation_id: Optional[str] = Field(default=None, description="Conversation ID for scoping")
34
+
35
+ def render(self) -> str:
36
+ """Render plan as formatted string with checkboxes."""
37
+ if not self.steps:
38
+ return "No plan created yet."
39
+
40
+ lines = []
41
+ if self.title:
42
+ lines.append(f"📋 {self.title}")
43
+
44
+ completed_count = sum(1 for s in self.steps if s.completed)
45
+ total_count = len(self.steps)
46
+ lines.append(f" Progress: {completed_count}/{total_count} steps completed")
47
+ lines.append("")
48
+
49
+ for i, step in enumerate(self.steps, 1):
50
+ checkbox = "☑" if step.completed else "☐"
51
+ status = " ✓" if step.completed else ""
52
+ lines.append(f" {checkbox} {i}. {step.description}{status}")
53
+
54
+ return "\n".join(lines)
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ """Convert to dictionary for serialization."""
58
+ return {
59
+ "title": self.title,
60
+ "steps": [{"description": s.description, "completed": s.completed} for s in self.steps],
61
+ "status": self.status,
62
+ "conversation_id": self.conversation_id
63
+ }
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: Dict[str, Any]) -> "PlanState":
67
+ """Create from dictionary."""
68
+ steps = [PlanStep(**s) for s in data.get("steps", [])]
69
+ return cls(
70
+ title=data.get("title", ""),
71
+ steps=steps,
72
+ status=data.get("status", "in_progress"),
73
+ conversation_id=data.get("conversation_id")
74
+ )
75
+
76
+
77
+ class FilesystemStorage:
78
+ """Filesystem-based plan storage for local CLI usage."""
79
+
80
+ def __init__(self, base_dir: Optional[str] = None):
81
+ """
82
+ Initialize filesystem storage.
83
+
84
+ Args:
85
+ base_dir: Base directory for plan storage.
86
+ Defaults to $ALITA_DIR/plans or .alita/plans
87
+ """
88
+ if base_dir:
89
+ self.base_dir = Path(base_dir)
90
+ else:
91
+ alita_dir = os.environ.get('ALITA_DIR', '.alita')
92
+ self.base_dir = Path(alita_dir) / 'plans'
93
+
94
+ self.base_dir.mkdir(parents=True, exist_ok=True)
95
+ logger.debug(f"Filesystem storage initialized at {self.base_dir}")
96
+
97
+ def _get_plan_path(self, conversation_id: str) -> Path:
98
+ """Get the path to a plan file."""
99
+ # Sanitize conversation_id for filesystem
100
+ safe_id = conversation_id.replace('/', '_').replace('\\', '_')
101
+ return self.base_dir / f"{safe_id}.json"
102
+
103
+ def get_plan(self, conversation_id: str) -> Optional[PlanState]:
104
+ """Load plan from filesystem."""
105
+ plan_path = self._get_plan_path(conversation_id)
106
+ if plan_path.exists():
107
+ try:
108
+ data = json.loads(plan_path.read_text())
109
+ return PlanState.from_dict(data)
110
+ except Exception as e:
111
+ logger.error(f"Failed to load plan from {plan_path}: {e}")
112
+ return None
113
+
114
+ def save_plan(self, conversation_id: str, plan: PlanState) -> bool:
115
+ """Save plan to filesystem."""
116
+ try:
117
+ plan_path = self._get_plan_path(conversation_id)
118
+ plan.conversation_id = conversation_id
119
+ plan_path.write_text(json.dumps(plan.to_dict(), indent=2))
120
+ logger.debug(f"Saved plan to {plan_path}")
121
+ return True
122
+ except Exception as e:
123
+ logger.error(f"Failed to save plan: {e}")
124
+ return False
125
+
126
+ def delete_plan(self, conversation_id: str) -> bool:
127
+ """Delete plan from filesystem."""
128
+ try:
129
+ plan_path = self._get_plan_path(conversation_id)
130
+ if plan_path.exists():
131
+ plan_path.unlink()
132
+ logger.debug(f"Deleted plan at {plan_path}")
133
+ return True
134
+ except Exception as e:
135
+ logger.error(f"Failed to delete plan: {e}")
136
+ return False
137
+
138
+
139
+ class PostgresStorage:
140
+ """PostgreSQL-based plan storage for production usage."""
141
+
142
+ def __init__(self, connection_string: str, conversation_id: Optional[str] = None):
143
+ """
144
+ Initialize PostgreSQL storage.
145
+
146
+ Args:
147
+ connection_string: PostgreSQL connection string
148
+ conversation_id: Conversation ID for scoping plans (from server or CLI session_id)
149
+ """
150
+ from sqlalchemy import create_engine
151
+ from sqlalchemy.orm import Session
152
+
153
+ self.connection_string = connection_string
154
+ self.conversation_id = conversation_id
155
+ self._engine = None
156
+ self._ensure_tables()
157
+
158
+ def _ensure_tables(self):
159
+ """Ensure the agent_plans table exists."""
160
+ from .models import ensure_plan_tables
161
+ ensure_plan_tables(self.connection_string)
162
+
163
+ def _get_engine(self):
164
+ """Get or create SQLAlchemy engine."""
165
+ if self._engine is None:
166
+ from sqlalchemy import create_engine
167
+ self._engine = create_engine(self.connection_string)
168
+ return self._engine
169
+
170
+ def _get_session(self):
171
+ """Get a database session."""
172
+ from sqlalchemy.orm import Session
173
+ return Session(self._get_engine())
174
+
175
+ def get_plan(self, conversation_id: str) -> Optional[PlanState]:
176
+ """
177
+ Load plan from PostgreSQL.
178
+
179
+ Uses conversation_id for scoping. Server provides conversation_id,
180
+ CLI provides session_id as conversation_id.
181
+ """
182
+ from .models import AgentPlan
183
+
184
+ try:
185
+ session = self._get_session()
186
+
187
+ # Use conversation_id for querying (set during initialization from server/CLI)
188
+ query_id = self.conversation_id or conversation_id
189
+
190
+ plan = session.query(AgentPlan).filter(
191
+ AgentPlan.conversation_id == query_id
192
+ ).first()
193
+
194
+ if plan:
195
+ steps = [
196
+ PlanStep(
197
+ description=s.get("description", ""),
198
+ completed=s.get("completed", False)
199
+ )
200
+ for s in plan.plan_data.get("steps", [])
201
+ ]
202
+ result = PlanState(
203
+ title=plan.title,
204
+ steps=steps,
205
+ status=plan.status,
206
+ conversation_id=query_id
207
+ )
208
+ session.close()
209
+ return result
210
+
211
+ session.close()
212
+ return None
213
+
214
+ except Exception as e:
215
+ logger.error(f"Failed to load plan from database: {e}")
216
+ return None
217
+
218
+ def save_plan(self, conversation_id: str, plan: PlanState) -> bool:
219
+ """
220
+ Save plan to PostgreSQL.
221
+
222
+ Uses conversation_id for scoping.
223
+ """
224
+ from .models import AgentPlan
225
+
226
+ try:
227
+ session = self._get_session()
228
+
229
+ # Use conversation_id for querying and storing
230
+ query_id = self.conversation_id or conversation_id
231
+
232
+ existing = session.query(AgentPlan).filter(
233
+ AgentPlan.conversation_id == query_id
234
+ ).first()
235
+
236
+ plan_data = {
237
+ "steps": [{"description": s.description, "completed": s.completed} for s in plan.steps]
238
+ }
239
+
240
+ if existing:
241
+ existing.title = plan.title
242
+ existing.plan_data = plan_data
243
+ existing.status = plan.status
244
+ existing.updated_at = datetime.utcnow()
245
+ else:
246
+ new_plan = AgentPlan(
247
+ conversation_id=query_id,
248
+ title=plan.title,
249
+ plan_data=plan_data,
250
+ status=plan.status
251
+ )
252
+ session.add(new_plan)
253
+
254
+ session.commit()
255
+ session.close()
256
+ return True
257
+
258
+ except Exception as e:
259
+ logger.error(f"Failed to save plan to database: {e}")
260
+ return False
261
+
262
+ def delete_plan(self, conversation_id: str) -> bool:
263
+ """
264
+ Delete plan from PostgreSQL.
265
+
266
+ Uses conversation_id for scoping.
267
+ """
268
+ from .models import AgentPlan
269
+
270
+ try:
271
+ session = self._get_session()
272
+
273
+ # Use conversation_id for querying
274
+ query_id = self.conversation_id or conversation_id
275
+
276
+ session.query(AgentPlan).filter(
277
+ AgentPlan.conversation_id == query_id
278
+ ).delete()
279
+ session.commit()
280
+ session.close()
281
+ return True
282
+
283
+ except Exception as e:
284
+ logger.error(f"Failed to delete plan from database: {e}")
285
+ return False
286
+
287
+
288
+ class PlanningWrapper(BaseModel):
289
+ """
290
+ Adaptive wrapper for plan management operations.
291
+
292
+ Automatically selects storage backend:
293
+ - PostgreSQL when connection_string is provided
294
+ - Filesystem when no connection_string (local usage)
295
+
296
+ Conversation ID can be:
297
+ 1. Passed explicitly to each method
298
+ 2. Set via conversation_id field (from server payload or CLI session_id)
299
+ """
300
+ connection_string: Optional[str] = Field(
301
+ default=None,
302
+ description="PostgreSQL connection string. If not provided, uses filesystem storage."
303
+ )
304
+ conversation_id: Optional[str] = Field(
305
+ default=None,
306
+ description="Optional conversation ID for scoping"
307
+ )
308
+ storage_dir: Optional[str] = Field(
309
+ default=None,
310
+ description="Directory for filesystem storage (when no connection_string)"
311
+ )
312
+ plan_callback: Optional[Any] = Field(
313
+ default=None,
314
+ description="Optional callback function called when plan changes (for CLI UI updates)"
315
+ )
316
+
317
+ # Runtime state
318
+ _storage: Any = None
319
+ _use_postgres: bool = False
320
+
321
+ class Config:
322
+ arbitrary_types_allowed = True
323
+
324
+ @model_validator(mode='after')
325
+ def setup_storage(self):
326
+ """Initialize the appropriate storage backend."""
327
+ conn_str = self.connection_string
328
+ if hasattr(conn_str, 'get_secret_value'):
329
+ conn_str = conn_str.get_secret_value()
330
+
331
+ if conn_str:
332
+ # Use PostgreSQL storage
333
+ try:
334
+ storage = PostgresStorage(conn_str, self.conversation_id)
335
+ object.__setattr__(self, '_storage', storage)
336
+ object.__setattr__(self, '_use_postgres', True)
337
+ logger.info("Planning toolkit using PostgreSQL storage")
338
+ except Exception as e:
339
+ logger.warning(f"Failed to initialize PostgreSQL storage, falling back to filesystem: {e}")
340
+ storage = FilesystemStorage(self.storage_dir)
341
+ object.__setattr__(self, '_storage', storage)
342
+ object.__setattr__(self, '_use_postgres', False)
343
+ else:
344
+ # Use filesystem storage
345
+ storage = FilesystemStorage(self.storage_dir)
346
+ object.__setattr__(self, '_storage', storage)
347
+ object.__setattr__(self, '_use_postgres', False)
348
+ logger.info("Planning toolkit using filesystem storage")
349
+
350
+ return self
351
+
352
+ def run(self, action: str, *args, **kwargs) -> str:
353
+ """Execute an action by name (called by BaseAction)."""
354
+ # Strip toolkit prefix if present (e.g., "Plan___update_plan" -> "update_plan")
355
+ if '___' in action:
356
+ action = action.split('___')[-1]
357
+
358
+ action_map = {
359
+ "update_plan": self.update_plan,
360
+ "complete_step": self.complete_step,
361
+ "get_plan_status": self.get_plan_status,
362
+ "delete_plan": self.delete_plan,
363
+ }
364
+
365
+ if action not in action_map:
366
+ return f"Unknown action: {action}"
367
+
368
+ return action_map[action](*args, **kwargs)
369
+
370
+ def update_plan(self, title: str, steps: List[str], conversation_id: Optional[str] = None) -> str:
371
+ """
372
+ Create or update an execution plan.
373
+
374
+ If a plan exists for the conversation_id, it will be replaced.
375
+
376
+ Args:
377
+ title: Plan title
378
+ steps: List of step descriptions
379
+ conversation_id: Conversation ID for scoping. Uses wrapper's conversation_id if not provided.
380
+
381
+ Returns:
382
+ Formatted plan state string
383
+ """
384
+ conversation_id = conversation_id or self.conversation_id
385
+ if not conversation_id:
386
+ return "❌ Error: conversation_id is required (from server or session_id from CLI)"
387
+
388
+ try:
389
+ plan = PlanState(
390
+ title=title,
391
+ steps=[PlanStep(description=s, completed=False) for s in steps],
392
+ status="in_progress",
393
+ conversation_id=conversation_id
394
+ )
395
+
396
+ existing = self._storage.get_plan(conversation_id)
397
+ if self._storage.save_plan(conversation_id, plan):
398
+ action = "updated" if existing else "created"
399
+
400
+ # Notify callback if set (for CLI UI updates)
401
+ if self.plan_callback:
402
+ self.plan_callback(plan)
403
+
404
+ return f"✓ Plan {action}:\n\n{plan.render()}"
405
+ else:
406
+ return "❌ Error: Failed to save plan"
407
+
408
+ except Exception as e:
409
+ logger.error(f"Failed to update plan: {e}")
410
+ return f"❌ Error updating plan: {str(e)}"
411
+
412
+ def complete_step(self, step_number: int, conversation_id: Optional[str] = None) -> str:
413
+ """
414
+ Mark a step as completed.
415
+
416
+ Args:
417
+ step_number: Step number (1-indexed)
418
+ conversation_id: Conversation ID for scoping. Uses wrapper's conversation_id if not provided.
419
+
420
+ Returns:
421
+ Updated plan state string
422
+ """
423
+ conversation_id = conversation_id or self.conversation_id
424
+ if not conversation_id:
425
+ return "❌ Error: conversation_id is required (from server or session_id from CLI)"
426
+
427
+ try:
428
+ plan = self._storage.get_plan(conversation_id)
429
+
430
+ if not plan or not plan.steps:
431
+ return "❌ No plan exists. Use update_plan first to create a plan."
432
+
433
+ if step_number < 1 or step_number > len(plan.steps):
434
+ return f"❌ Invalid step number. Plan has {len(plan.steps)} steps (1-{len(plan.steps)})."
435
+
436
+ step = plan.steps[step_number - 1]
437
+ if step.completed:
438
+ return f"Step {step_number} was already completed.\n\n{plan.render()}"
439
+
440
+ step.completed = True
441
+
442
+ # Check if all steps completed
443
+ all_completed = all(s.completed for s in plan.steps)
444
+ if all_completed:
445
+ plan.status = "completed"
446
+
447
+ if self._storage.save_plan(conversation_id, plan):
448
+ # Notify callback if set (for CLI UI updates)
449
+ if self.plan_callback:
450
+ self.plan_callback(plan)
451
+
452
+ completed = sum(1 for s in plan.steps if s.completed)
453
+ total = len(plan.steps)
454
+ return f"✓ Step {step_number} completed ({completed}/{total} done)\n\n{plan.render()}"
455
+ else:
456
+ return "❌ Error: Failed to save plan progress"
457
+
458
+ except Exception as e:
459
+ logger.error(f"Failed to complete step: {e}")
460
+ return f"❌ Error completing step: {str(e)}"
461
+
462
+ def get_plan_status(self, conversation_id: Optional[str] = None) -> str:
463
+ """
464
+ Get the current plan status.
465
+
466
+ Args:
467
+ conversation_id: Conversation ID for scoping. Uses wrapper's conversation_id if not provided.
468
+
469
+ Returns:
470
+ Formatted plan state or message if no plan exists
471
+ """
472
+ conversation_id = conversation_id or self.conversation_id
473
+ if not conversation_id:
474
+ return "❌ Error: conversation_id is required (from server or session_id from CLI)"
475
+
476
+ try:
477
+ plan = self._storage.get_plan(conversation_id)
478
+
479
+ if not plan:
480
+ return "No plan exists for the current conversation. Use update_plan to create one."
481
+
482
+ return plan.render()
483
+
484
+ except Exception as e:
485
+ logger.error(f"Failed to get plan status: {e}")
486
+ return f"❌ Error getting plan status: {str(e)}"
487
+
488
+ def delete_plan(self, conversation_id: Optional[str] = None) -> str:
489
+ """
490
+ Delete the current plan.
491
+
492
+ Args:
493
+ conversation_id: Conversation ID for scoping. Uses wrapper's conversation_id if not provided.
494
+
495
+ Returns:
496
+ Confirmation message
497
+ """
498
+ conversation_id = conversation_id or self.conversation_id
499
+ if not conversation_id:
500
+ return "❌ Error: conversation_id is required (from server or session_id from CLI)"
501
+
502
+ try:
503
+ plan = self._storage.get_plan(conversation_id)
504
+
505
+ if not plan:
506
+ return "No plan exists for the current conversation."
507
+
508
+ if self._storage.delete_plan(conversation_id):
509
+ return f"✓ Plan '{plan.title}' deleted successfully."
510
+ else:
511
+ return "❌ Error: Failed to delete plan"
512
+
513
+ except Exception as e:
514
+ logger.error(f"Failed to delete plan: {e}")
515
+ return f"❌ Error deleting plan: {str(e)}"
516
+
517
+ def get_available_tools(self) -> List[Dict[str, Any]]:
518
+ """
519
+ Return list of available planning tools with their schemas.
520
+
521
+ Returns:
522
+ List of tool definitions with name, description, and args_schema
523
+ """
524
+ # Define input schemas for tools
525
+ # conversation_id is optional when set on the wrapper instance
526
+ UpdatePlanInput = create_model(
527
+ 'UpdatePlanInput',
528
+ title=(str, Field(description="Title for the plan (e.g., 'Test Investigation Plan')")),
529
+ steps=(List[str], Field(description="List of step descriptions in order")),
530
+ conversation_id=(Optional[str], Field(default=None, description="Conversation ID (optional - uses default if not provided)"))
531
+ )
532
+
533
+ CompleteStepInput = create_model(
534
+ 'CompleteStepInput',
535
+ step_number=(int, Field(description="Step number to mark as complete (1-indexed)")),
536
+ conversation_id=(Optional[str], Field(default=None, description="Conversation ID (optional - uses default if not provided)"))
537
+ )
538
+
539
+ GetPlanStatusInput = create_model(
540
+ 'GetPlanStatusInput',
541
+ conversation_id=(Optional[str], Field(default=None, description="Conversation ID (optional - uses default if not provided)"))
542
+ )
543
+
544
+ DeletePlanInput = create_model(
545
+ 'DeletePlanInput',
546
+ conversation_id=(Optional[str], Field(default=None, description="Conversation ID (optional - uses default if not provided)"))
547
+ )
548
+
549
+ return [
550
+ {
551
+ "name": "update_plan",
552
+ "description": """Create or replace the current execution plan.
553
+
554
+ Use this when:
555
+ - Starting a multi-step task that needs tracking
556
+ - The sequence of activities matters
557
+ - Breaking down a complex task into phases
558
+
559
+ The plan will be displayed and you can mark steps complete as you progress.
560
+
561
+ Example:
562
+ update_plan(
563
+ title="API Test Investigation",
564
+ steps=[
565
+ "Reproduce the failing test locally",
566
+ "Capture error logs and stack trace",
567
+ "Identify root cause",
568
+ "Apply fix",
569
+ "Re-run test suite"
570
+ ]
571
+ )""",
572
+ "args_schema": UpdatePlanInput
573
+ },
574
+ {
575
+ "name": "complete_step",
576
+ "description": """Mark a step in the current plan as completed.
577
+
578
+ Use this after finishing a step to update progress.
579
+ Step numbers are 1-indexed (first step is 1, not 0).
580
+
581
+ Example:
582
+ complete_step(step_number=1) # Mark first step as done""",
583
+ "args_schema": CompleteStepInput
584
+ },
585
+ {
586
+ "name": "get_plan_status",
587
+ "description": """Get the current plan status and progress.
588
+
589
+ Shows the plan title, all steps with completion status, and overall progress.
590
+ Use this to review what needs to be done or verify progress.""",
591
+ "args_schema": GetPlanStatusInput
592
+ },
593
+ {
594
+ "name": "delete_plan",
595
+ "description": """Delete the current plan.
596
+
597
+ Use this when:
598
+ - The plan is complete and no longer needed
599
+ - You want to start fresh with a new plan
600
+ - The current plan is no longer relevant""",
601
+ "args_schema": DeletePlanInput
602
+ }
603
+ ]
604
+
605
+
606
+ # Import create_model for get_available_tools
607
+ from pydantic import create_model
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import re
2
3
  from typing import Any, Optional, Union, List
3
4
  from langchain_core.runnables import RunnableConfig
4
5
  from langchain_core.tools import BaseTool
@@ -23,7 +24,7 @@ class RouterNode(BaseTool):
23
24
  result = template.evaluate()
24
25
  logger.info(f"RouterNode evaluated condition '{self.condition}' with input {input_data} => {result}")
25
26
  result = clean_string(str(result))
26
- if result in self.routes:
27
+ if result in [clean_string(formatted_result) for formatted_result in self.routes]:
27
28
  # If the result is one of the routes, return it
28
29
  return {"router_output": result}
29
30
  elif result == self.default_output: