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
@@ -68,6 +68,18 @@ class CLICallbackHandler(BaseCallbackHandler):
68
68
  self.pending_tokens: Dict[str, List[str]] = defaultdict(list)
69
69
  self.current_model: str = ""
70
70
  self.step_counter: int = 0
71
+
72
+ # External status spinner that can be stopped
73
+ self.status = None
74
+
75
+ def _stop_status(self):
76
+ """Stop the external status spinner if set."""
77
+ if self.status is not None:
78
+ try:
79
+ self.status.stop()
80
+ self.status = None
81
+ except Exception:
82
+ pass
71
83
 
72
84
  def _format_json_content(self, data: Any, max_length: int = 1500) -> str:
73
85
  """Format data as pretty JSON string."""
@@ -137,6 +149,9 @@ class CLICallbackHandler(BaseCallbackHandler):
137
149
  **kwargs: Any,
138
150
  ) -> None:
139
151
  """Called when a tool starts running."""
152
+ # Stop the thinking spinner when a tool starts
153
+ self._stop_status()
154
+
140
155
  if not self.show_tool_outputs:
141
156
  return
142
157
 
@@ -390,23 +405,26 @@ class CLICallbackHandler(BaseCallbackHandler):
390
405
  llm_run_id = str(run_id)
391
406
  llm_info = self.llm_runs.pop(llm_run_id, {})
392
407
 
393
- # Show any pending tokens (streaming output)
394
- if self.show_thinking:
395
- tokens = self.pending_tokens.pop(llm_run_id, [])
396
- if tokens:
397
- thinking_text = "".join(tokens)
398
- if thinking_text.strip():
399
- # Show thinking in a subtle panel
400
- max_len = 600
401
- display_text = thinking_text[:max_len] + ('...' if len(thinking_text) > max_len else '')
402
- console.print(Panel(
403
- Text(display_text, style="dim italic"),
404
- title="[dim]💭 Thinking[/dim]",
405
- title_align="left",
406
- border_style="dim",
407
- box=box.SIMPLE,
408
- padding=(0, 1),
409
- ))
408
+ # Clear pending tokens - we don't show them as "Thinking" anymore
409
+ # The final response will be displayed by the main chat loop
410
+ # Only show thinking if there were tool calls (indicated by having active tool runs)
411
+ tokens = self.pending_tokens.pop(llm_run_id, [])
412
+
413
+ # Only show thinking panel if we have active tool context (intermediate reasoning)
414
+ if self.show_thinking and tokens and len(self.tool_runs) > 0:
415
+ thinking_text = "".join(tokens)
416
+ if thinking_text.strip():
417
+ # Show thinking in a subtle panel
418
+ max_len = 600
419
+ display_text = thinking_text[:max_len] + ('...' if len(thinking_text) > max_len else '')
420
+ console.print(Panel(
421
+ Text(display_text, style="dim italic"),
422
+ title="[dim]💭 Thinking[/dim]",
423
+ title_align="left",
424
+ border_style="dim",
425
+ box=box.SIMPLE,
426
+ padding=(0, 1),
427
+ ))
410
428
 
411
429
  if self.show_llm_calls and llm_info:
412
430
  start_time = llm_info.get("start_time")
@@ -425,15 +443,68 @@ class CLICallbackHandler(BaseCallbackHandler):
425
443
  **kwargs: Any,
426
444
  ) -> None:
427
445
  """Called when LLM errors."""
446
+ error_str = str(error)
447
+
448
+ # Parse common error patterns for user-friendly messages
449
+ user_message = None
450
+ hint = None
451
+
452
+ # Invalid model identifier (Bedrock/Claude)
453
+ if "model identifier is invalid" in error_str.lower() or "BedrockException" in error_str:
454
+ user_message = "Invalid model identifier"
455
+ hint = "The model may not be available in your region or the model ID is incorrect.\nUse /model to switch to a different model."
456
+
457
+ # Rate limiting
458
+ elif "rate limit" in error_str.lower() or "too many requests" in error_str.lower():
459
+ user_message = "Rate limit exceeded"
460
+ hint = "Wait a moment and try again, or switch to a different model with /model."
461
+
462
+ # Token/context length exceeded
463
+ elif "context length" in error_str.lower() or "maximum.*tokens" in error_str.lower() or "too long" in error_str.lower():
464
+ user_message = "Context length exceeded"
465
+ hint = "The conversation is too long. Start a new session or use /clear to reset."
466
+
467
+ # Authentication errors
468
+ elif "authentication" in error_str.lower() or "unauthorized" in error_str.lower() or "api key" in error_str.lower():
469
+ user_message = "Authentication failed"
470
+ hint = "Check your API credentials in the configuration."
471
+
472
+ # Model not found/available
473
+ elif "model not found" in error_str.lower() or "does not exist" in error_str.lower():
474
+ user_message = "Model not found"
475
+ hint = "The requested model is not available. Use /model to select a different one."
476
+
477
+ # Build the display message
428
478
  console.print()
429
- console.print(Panel(
430
- Text(str(error), style="red"),
431
- title="[bold red]✗ LLM Error[/bold red]",
432
- title_align="left",
433
- border_style="red",
434
- box=ERROR_BOX,
435
- padding=(0, 1),
436
- ))
479
+ if user_message:
480
+ content = Text()
481
+ content.append(f" {user_message}\n\n", style="bold red")
482
+ if hint:
483
+ content.append(f"💡 {hint}\n\n", style="yellow")
484
+ content.append("Technical details:\n", style="dim")
485
+ # Truncate long error messages
486
+ if len(error_str) > 300:
487
+ content.append(error_str[:300] + "...", style="dim")
488
+ else:
489
+ content.append(error_str, style="dim")
490
+ console.print(Panel(
491
+ content,
492
+ title="[bold red]✗ LLM Error[/bold red]",
493
+ title_align="left",
494
+ border_style="red",
495
+ box=ERROR_BOX,
496
+ padding=(0, 1),
497
+ ))
498
+ else:
499
+ # Fallback to original behavior for unrecognized errors
500
+ console.print(Panel(
501
+ Text(str(error), style="red"),
502
+ title="[bold red]✗ LLM Error[/bold red]",
503
+ title_align="left",
504
+ border_style="red",
505
+ box=ERROR_BOX,
506
+ padding=(0, 1),
507
+ ))
437
508
 
438
509
  #
439
510
  # Chain Callbacks
alita_sdk/cli/cli.py CHANGED
@@ -30,10 +30,11 @@ logger = logging.getLogger(__name__)
30
30
  @click.group()
31
31
  @click.option('--env-file', default='.env', help='Path to .env file')
32
32
  @click.option('--debug', is_flag=True, help='Enable debug logging')
33
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose/info logging (shows timing)')
33
34
  @click.option('--output', type=click.Choice(['text', 'json']), default='text',
34
35
  help='Output format')
35
36
  @click.pass_context
36
- def cli(ctx, env_file: str, debug: bool, output: str):
37
+ def cli(ctx, env_file: str, debug: bool, verbose: bool, output: str):
37
38
  """
38
39
  Alita SDK CLI - Test agents and toolkits from the command line.
39
40
 
@@ -55,12 +56,18 @@ def cli(ctx, env_file: str, debug: bool, output: str):
55
56
  logging.getLogger('alita_sdk').setLevel(logging.DEBUG)
56
57
  logger.setLevel(logging.DEBUG)
57
58
  logger.debug("Debug logging enabled")
59
+ elif verbose:
60
+ # Verbose mode shows INFO level (timing info)
61
+ logging.getLogger('alita_sdk').setLevel(logging.INFO)
62
+ logger.setLevel(logging.INFO)
63
+ logger.info("Verbose logging enabled")
58
64
 
59
65
  # Load configuration
60
66
  config = get_config(env_file=env_file)
61
67
  ctx.obj['config'] = config
62
68
  ctx.obj['formatter'] = get_formatter(output)
63
69
  ctx.obj['debug'] = debug
70
+ ctx.obj['verbose'] = verbose
64
71
 
65
72
  # Check if configuration is valid (but don't fail yet - some commands don't need it)
66
73
  if not config.is_configured():
@@ -128,10 +135,12 @@ def config(ctx):
128
135
  # Import subcommands
129
136
  from . import toolkit
130
137
  from . import agents
138
+ from . import inventory
131
139
 
132
140
  # Register subcommands
133
141
  cli.add_command(toolkit.toolkit)
134
142
  cli.add_command(agents.agent)
143
+ cli.add_command(inventory.inventory)
135
144
 
136
145
  # Add top-level 'chat' command as alias to 'agent chat'
137
146
  cli.add_command(agents.agent_chat, name='chat')
alita_sdk/cli/config.py CHANGED
@@ -7,6 +7,7 @@ as the SDK tests and Streamlit interface.
7
7
 
8
8
  import os
9
9
  import re
10
+ import json
10
11
  from typing import Optional, Dict, Any
11
12
  from dotenv import load_dotenv
12
13
  import logging
@@ -22,37 +23,140 @@ class CLIConfig:
22
23
  Initialize CLI configuration.
23
24
 
24
25
  Args:
25
- env_file: Path to .env file. If None, uses default (.env in current directory)
26
+ env_file: Path to .env file. If None, checks ALITA_ENV_FILE env var,
27
+ then falls back to .alita/.env or .env in current directory
26
28
  """
27
- self.env_file = env_file or '.env'
29
+ self._config_json: Dict[str, Any] = {}
30
+
31
+ if env_file:
32
+ self.env_file = env_file
33
+ else:
34
+ # Check ALITA_ENV_FILE environment variable first
35
+ alita_env_file = os.getenv('ALITA_ENV_FILE')
36
+ if alita_env_file:
37
+ # Expand ~ and resolve path
38
+ expanded_path = os.path.expanduser(alita_env_file)
39
+ if os.path.exists(expanded_path):
40
+ self.env_file = expanded_path
41
+ else:
42
+ logger.warning(f"ALITA_ENV_FILE set to {alita_env_file} but file not found")
43
+ self.env_file = expanded_path # Still use it, will warn later
44
+ elif os.path.exists(os.path.expanduser('~/.alita/.env')):
45
+ self.env_file = os.path.expanduser('~/.alita/.env')
46
+ elif os.path.exists('.alita/.env'):
47
+ self.env_file = '.alita/.env'
48
+ else:
49
+ self.env_file = '.env'
28
50
  self._load_env()
51
+ self._load_config_json()
29
52
 
30
53
  def _load_env(self):
31
54
  """Load environment variables from .env file."""
32
55
  if os.path.exists(self.env_file):
33
- load_dotenv(self.env_file)
56
+ # Use override=True to ensure .env values take precedence
57
+ load_dotenv(self.env_file, override=True)
34
58
  logger.debug(f"Loaded environment from {self.env_file}")
35
59
  else:
36
60
  logger.debug(f"No .env file found at {self.env_file}, using system environment")
37
61
 
62
+ def _load_config_json(self):
63
+ """Load configuration from $ALITA_DIR/config.json as fallback."""
64
+ # Try ALITA_DIR from env, then ~/.alita, then .alita
65
+ alita_dir = os.getenv('ALITA_DIR')
66
+ if alita_dir:
67
+ config_path = os.path.join(os.path.expanduser(alita_dir), 'config.json')
68
+ else:
69
+ # Try ~/.alita/config.json first, then .alita/config.json
70
+ home_config = os.path.expanduser('~/.alita/config.json')
71
+ local_config = '.alita/config.json'
72
+ if os.path.exists(home_config):
73
+ config_path = home_config
74
+ elif os.path.exists(local_config):
75
+ config_path = local_config
76
+ else:
77
+ config_path = home_config # Default path even if doesn't exist
78
+
79
+ if os.path.exists(config_path):
80
+ try:
81
+ with open(config_path, 'r') as f:
82
+ self._config_json = json.load(f)
83
+ logger.debug(f"Loaded config from {config_path}")
84
+
85
+ # Load env section into environment variables
86
+ self._load_env_section()
87
+ except (json.JSONDecodeError, IOError) as e:
88
+ logger.warning(f"Failed to load config.json: {e}")
89
+
90
+ def _load_env_section(self):
91
+ """Load variables from config.json 'env' section into environment."""
92
+ env_vars = self._config_json.get('env', {})
93
+ if isinstance(env_vars, dict):
94
+ for key, value in env_vars.items():
95
+ # Only set if not already in environment (env vars take precedence)
96
+ if key not in os.environ and value is not None:
97
+ os.environ[key] = str(value)
98
+ logger.debug(f"Set {key} from config.json env section")
99
+
100
+ def _get_config_value(self, env_key: str, json_key: Optional[str] = None) -> Optional[str]:
101
+ """
102
+ Get config value from environment first, then config.json fallback.
103
+
104
+ Args:
105
+ env_key: Environment variable name
106
+ json_key: Key in config.json (defaults to lowercase of env_key)
107
+ """
108
+ # Try environment variable first
109
+ value = os.getenv(env_key)
110
+ if value:
111
+ return value
112
+
113
+ # Fallback to config.json
114
+ if json_key is None:
115
+ json_key = env_key.lower()
116
+
117
+ return self._config_json.get(json_key)
118
+
38
119
  @property
39
120
  def deployment_url(self) -> Optional[str]:
40
- """Get deployment URL from environment."""
41
- return os.getenv('DEPLOYMENT_URL')
121
+ """Get deployment URL from environment or config.json."""
122
+ return self._get_config_value('DEPLOYMENT_URL', 'deployment_url')
42
123
 
43
124
  @property
44
125
  def project_id(self) -> Optional[int]:
45
- """Get project ID from environment."""
126
+ """Get project ID from environment or config.json."""
46
127
  try:
47
- value = os.getenv('PROJECT_ID')
128
+ value = self._get_config_value('PROJECT_ID', 'project_id')
48
129
  return int(value) if value else None
49
130
  except (TypeError, ValueError):
50
131
  return None
51
132
 
52
133
  @property
53
134
  def api_key(self) -> Optional[str]:
54
- """Get API key from environment."""
55
- return os.getenv('API_KEY')
135
+ """Get API key from environment or config.json."""
136
+ return self._get_config_value('API_KEY', 'api_key')
137
+
138
+ @property
139
+ def default_model(self) -> Optional[str]:
140
+ """Get default model from environment or config.json."""
141
+ return self._get_config_value('ALITA_DEFAULT_MODEL', 'default_model')
142
+
143
+ @property
144
+ def default_temperature(self) -> Optional[float]:
145
+ """Get default temperature from environment or config.json."""
146
+ try:
147
+ value = self._get_config_value('ALITA_DEFAULT_TEMPERATURE', 'default_temperature')
148
+ return float(value) if value is not None else None
149
+ except (TypeError, ValueError):
150
+ return None
151
+
152
+ @property
153
+ def default_max_tokens(self) -> Optional[int]:
154
+ """Get default max tokens from environment or config.json."""
155
+ try:
156
+ value = self._get_config_value('ALITA_DEFAULT_MAX_TOKENS', 'default_max_tokens')
157
+ return int(value) if value is not None else None
158
+ except (TypeError, ValueError):
159
+ return None
56
160
 
57
161
  @property
58
162
  def alita_dir(self) -> str:
@@ -83,6 +187,55 @@ class CLIConfig:
83
187
  return 'mcp.json'
84
188
  return alita_mcp
85
189
 
190
+ @property
191
+ def context_management(self) -> Dict[str, Any]:
192
+ """
193
+ Get context management configuration from config.json.
194
+
195
+ Returns configuration for chat history context management with defaults:
196
+ - enabled: True - Enable context management
197
+ - max_context_tokens: 8000 - Maximum tokens in context
198
+ - preserve_recent_messages: 5 - Always keep N most recent messages
199
+ - pruning_method: 'oldest_first' - Strategy for pruning (oldest_first, importance_based)
200
+ - enable_summarization: True - Generate summaries of pruned messages
201
+ - summary_trigger_ratio: 0.8 - Trigger summarization at 80% context fill
202
+ - summaries_limit_count: 5 - Maximum number of summaries to keep
203
+ - session_max_age_days: 30 - Purge sessions older than N days
204
+ - max_sessions: 50 - Maximum number of sessions to keep
205
+ """
206
+ defaults = {
207
+ 'enabled': True,
208
+ 'max_context_tokens': 8000,
209
+ 'preserve_recent_messages': 5,
210
+ 'pruning_method': 'oldest_first',
211
+ 'enable_summarization': True,
212
+ 'summary_trigger_ratio': 0.8,
213
+ 'summaries_limit_count': 5,
214
+ 'session_max_age_days': 30,
215
+ 'max_sessions': 50,
216
+ 'weights': {
217
+ 'recency': 1.0,
218
+ 'importance': 1.0,
219
+ 'user_messages': 1.2,
220
+ 'thread_continuity': 1.0,
221
+ },
222
+ }
223
+
224
+ # Get from config.json
225
+ config = self._config_json.get('context_management', {})
226
+
227
+ # Merge with defaults
228
+ result = defaults.copy()
229
+ if isinstance(config, dict):
230
+ result.update(config)
231
+
232
+ return result
233
+
234
+ @property
235
+ def sessions_dir(self) -> str:
236
+ """Get sessions directory (derived from ALITA_DIR)."""
237
+ return os.path.join(self.alita_dir, 'sessions')
238
+
86
239
  def is_configured(self) -> bool:
87
240
  """Check if all required configuration is present."""
88
241
  return all([
@@ -0,0 +1,30 @@
1
+ """
2
+ Context management for CLI chat history.
3
+
4
+ Provides token-aware context pruning, summarization, and session management
5
+ to optimize LLM context usage during CLI conversations.
6
+ """
7
+
8
+ from .manager import CLIContextManager, sanitize_message_history
9
+ from .message import CLIMessage
10
+ from .token_estimation import estimate_tokens, calculate_total_tokens
11
+ from .strategies import (
12
+ PruningStrategy,
13
+ OldestFirstStrategy,
14
+ ImportanceBasedStrategy,
15
+ PruningStrategyFactory,
16
+ )
17
+ from .cleanup import purge_old_sessions
18
+
19
+ __all__ = [
20
+ 'CLIContextManager',
21
+ 'CLIMessage',
22
+ 'estimate_tokens',
23
+ 'calculate_total_tokens',
24
+ 'PruningStrategy',
25
+ 'OldestFirstStrategy',
26
+ 'ImportanceBasedStrategy',
27
+ 'PruningStrategyFactory',
28
+ 'purge_old_sessions',
29
+ 'sanitize_message_history',
30
+ ]
@@ -0,0 +1,198 @@
1
+ """
2
+ Session cleanup utilities for CLI context management.
3
+
4
+ Handles purging old sessions to prevent disk space issues.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ import logging
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import List, Optional, Tuple
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_sessions_dir(alita_dir: str = '.alita') -> Path:
18
+ """
19
+ Get the sessions directory path.
20
+
21
+ Args:
22
+ alita_dir: Base Alita directory
23
+
24
+ Returns:
25
+ Path to sessions directory
26
+ """
27
+ # Expand ~ for home directory
28
+ if alita_dir.startswith('~'):
29
+ alita_dir = os.path.expanduser(alita_dir)
30
+ return Path(alita_dir) / 'sessions'
31
+
32
+
33
+ def list_sessions_with_age(alita_dir: str = '.alita') -> List[Tuple[str, float, datetime]]:
34
+ """
35
+ List all sessions with their age in days.
36
+
37
+ Args:
38
+ alita_dir: Base Alita directory
39
+
40
+ Returns:
41
+ List of tuples: (session_id, age_days, modified_time)
42
+ """
43
+ sessions_dir = get_sessions_dir(alita_dir)
44
+
45
+ if not sessions_dir.exists():
46
+ return []
47
+
48
+ sessions = []
49
+ now = datetime.now(timezone.utc)
50
+
51
+ for session_path in sessions_dir.iterdir():
52
+ if not session_path.is_dir():
53
+ continue
54
+
55
+ session_id = session_path.name
56
+
57
+ # Get modification time (most recent file in session)
58
+ try:
59
+ mtime = max(
60
+ f.stat().st_mtime
61
+ for f in session_path.rglob('*')
62
+ if f.is_file()
63
+ )
64
+ modified = datetime.fromtimestamp(mtime, tz=timezone.utc)
65
+ age_days = (now - modified).total_seconds() / 86400
66
+ sessions.append((session_id, age_days, modified))
67
+ except (ValueError, OSError):
68
+ # No files or error accessing - use directory mtime
69
+ try:
70
+ mtime = session_path.stat().st_mtime
71
+ modified = datetime.fromtimestamp(mtime, tz=timezone.utc)
72
+ age_days = (now - modified).total_seconds() / 86400
73
+ sessions.append((session_id, age_days, modified))
74
+ except OSError:
75
+ continue
76
+
77
+ # Sort by modification time (oldest first)
78
+ sessions.sort(key=lambda x: x[2])
79
+
80
+ return sessions
81
+
82
+
83
+ def purge_old_sessions(
84
+ max_age_days: int = 30,
85
+ max_sessions: int = 50,
86
+ alita_dir: str = '.alita',
87
+ dry_run: bool = False
88
+ ) -> Tuple[int, int]:
89
+ """
90
+ Purge old sessions based on age and count limits.
91
+
92
+ Args:
93
+ max_age_days: Maximum age in days before a session is purged
94
+ max_sessions: Maximum number of sessions to keep
95
+ alita_dir: Base Alita directory
96
+ dry_run: If True, only report what would be deleted
97
+
98
+ Returns:
99
+ Tuple of (sessions_deleted, bytes_freed)
100
+ """
101
+ sessions_dir = get_sessions_dir(alita_dir)
102
+
103
+ if not sessions_dir.exists():
104
+ return 0, 0
105
+
106
+ sessions = list_sessions_with_age(alita_dir)
107
+
108
+ if not sessions:
109
+ return 0, 0
110
+
111
+ to_delete = []
112
+
113
+ # Mark sessions older than max_age_days for deletion
114
+ for session_id, age_days, modified in sessions:
115
+ if age_days > max_age_days:
116
+ to_delete.append(session_id)
117
+
118
+ # If we still have too many sessions, delete oldest ones
119
+ remaining = [s for s in sessions if s[0] not in to_delete]
120
+ if len(remaining) > max_sessions:
121
+ # Sort remaining by age (oldest first) and mark excess for deletion
122
+ excess_count = len(remaining) - max_sessions
123
+ for session_id, age_days, modified in remaining[:excess_count]:
124
+ if session_id not in to_delete:
125
+ to_delete.append(session_id)
126
+
127
+ # Delete marked sessions
128
+ deleted_count = 0
129
+ bytes_freed = 0
130
+
131
+ for session_id in to_delete:
132
+ session_path = sessions_dir / session_id
133
+
134
+ if not session_path.exists():
135
+ continue
136
+
137
+ # Calculate size before deletion
138
+ try:
139
+ session_size = sum(
140
+ f.stat().st_size
141
+ for f in session_path.rglob('*')
142
+ if f.is_file()
143
+ )
144
+ except OSError:
145
+ session_size = 0
146
+
147
+ if dry_run:
148
+ logger.info(f"Would delete session: {session_id} ({session_size} bytes)")
149
+ else:
150
+ try:
151
+ shutil.rmtree(session_path)
152
+ deleted_count += 1
153
+ bytes_freed += session_size
154
+ logger.debug(f"Deleted session: {session_id}")
155
+ except OSError as e:
156
+ logger.warning(f"Failed to delete session {session_id}: {e}")
157
+
158
+ if deleted_count > 0:
159
+ logger.info(f"Purged {deleted_count} old sessions, freed {bytes_freed} bytes")
160
+
161
+ return deleted_count, bytes_freed
162
+
163
+
164
+ def get_session_disk_usage(alita_dir: str = '.alita') -> Tuple[int, int]:
165
+ """
166
+ Get disk usage statistics for sessions.
167
+
168
+ Args:
169
+ alita_dir: Base Alita directory
170
+
171
+ Returns:
172
+ Tuple of (session_count, total_bytes)
173
+ """
174
+ sessions_dir = get_sessions_dir(alita_dir)
175
+
176
+ if not sessions_dir.exists():
177
+ return 0, 0
178
+
179
+ session_count = 0
180
+ total_bytes = 0
181
+
182
+ for session_path in sessions_dir.iterdir():
183
+ if not session_path.is_dir():
184
+ continue
185
+
186
+ session_count += 1
187
+
188
+ try:
189
+ session_size = sum(
190
+ f.stat().st_size
191
+ for f in session_path.rglob('*')
192
+ if f.is_file()
193
+ )
194
+ total_bytes += session_size
195
+ except OSError:
196
+ continue
197
+
198
+ return session_count, total_bytes