alita-sdk 0.3.462__py3-none-any.whl → 0.3.627__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +15 -3
- alita_sdk/cli/agent_loader.py +56 -8
- alita_sdk/cli/agent_ui.py +93 -31
- alita_sdk/cli/agents.py +2274 -230
- alita_sdk/cli/callbacks.py +96 -25
- alita_sdk/cli/cli.py +10 -1
- alita_sdk/cli/config.py +162 -9
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +14 -17
- alita_sdk/cli/toolkit_loader.py +35 -5
- alita_sdk/cli/tools/__init__.py +36 -2
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +910 -64
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +0 -3
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +17 -5
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +353 -48
- alita_sdk/runtime/clients/sandbox_client.py +0 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +123 -26
- alita_sdk/runtime/langchain/constants.py +642 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +279 -73
- alita_sdk/runtime/langchain/utils.py +82 -15
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +7 -0
- alita_sdk/runtime/toolkits/application.py +21 -9
- alita_sdk/runtime/toolkits/artifact.py +15 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +139 -251
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +238 -32
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/application.py +20 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +43 -15
- alita_sdk/runtime/tools/image_generation.py +50 -44
- alita_sdk/runtime/tools/llm.py +852 -67
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +9 -6
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +7 -2
- alita_sdk/runtime/tools/vectorstore_base.py +51 -11
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +202 -5
- alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +6 -10
- alita_sdk/runtime/utils/toolkit_utils.py +16 -5
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +113 -29
- alita_sdk/tools/ado/repos/__init__.py +51 -33
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -9
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +170 -45
- alita_sdk/tools/bitbucket/__init__.py +17 -12
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +10 -7
- alita_sdk/tools/code_indexer_toolkit.py +73 -23
- alita_sdk/tools/confluence/__init__.py +21 -15
- alita_sdk/tools/confluence/api_wrapper.py +78 -23
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +13 -14
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +15 -11
- alita_sdk/tools/gitlab/api_wrapper.py +207 -41
- alita_sdk/tools/gitlab_org/__init__.py +10 -8
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +10 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -11
- alita_sdk/tools/jira/api_wrapper.py +91 -40
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +11 -3
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +490 -114
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/pptx/__init__.py +10 -9
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +30 -10
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +10 -8
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -9
- alita_sdk/tools/salesforce/__init__.py +10 -9
- alita_sdk/tools/servicenow/__init__.py +17 -14
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -8
- alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
- alita_sdk/tools/slack/__init__.py +10 -8
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +11 -9
- alita_sdk/tools/testio/__init__.py +10 -8
- alita_sdk/tools/testrail/__init__.py +11 -8
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
- alita_sdk/tools/xray/__init__.py +12 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +9 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
- alita_sdk/tools/zephyr_essential/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -9
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -8
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.462.dist-info/RECORD +0 -384
- alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
alita_sdk/cli/callbacks.py
CHANGED
|
@@ -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
|
-
#
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
430
|
-
Text(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|