alita-sdk 0.3.257__py3-none-any.whl → 0.3.584__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +215 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3794 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- 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/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- 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 +11 -0
- alita_sdk/configurations/ado.py +148 -2
- alita_sdk/configurations/azure_search.py +1 -1
- alita_sdk/configurations/bigquery.py +1 -1
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/browser.py +18 -0
- alita_sdk/configurations/carrier.py +19 -0
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/delta_lake.py +1 -1
- alita_sdk/configurations/figma.py +76 -5
- alita_sdk/configurations/github.py +65 -1
- alita_sdk/configurations/gitlab.py +81 -0
- alita_sdk/configurations/google_places.py +17 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +323 -0
- alita_sdk/configurations/postman.py +1 -1
- alita_sdk/configurations/qtest.py +72 -3
- alita_sdk/configurations/report_portal.py +115 -0
- alita_sdk/configurations/salesforce.py +19 -0
- alita_sdk/configurations/service_now.py +1 -12
- alita_sdk/configurations/sharepoint.py +167 -0
- alita_sdk/configurations/sonar.py +18 -0
- alita_sdk/configurations/sql.py +20 -0
- alita_sdk/configurations/testio.py +101 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +94 -1
- alita_sdk/configurations/zephyr_enterprise.py +94 -1
- alita_sdk/configurations/zephyr_essential.py +95 -0
- alita_sdk/runtime/clients/artifact.py +21 -4
- alita_sdk/runtime/clients/client.py +458 -67
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +352 -0
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +183 -43
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
- alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
- alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
- alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
- alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
- alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
- alita_sdk/runtime/langchain/langraph_agent.py +493 -105
- alita_sdk/runtime/langchain/utils.py +118 -8
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- 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 +28 -0
- alita_sdk/runtime/toolkits/application.py +14 -4
- alita_sdk/runtime/toolkits/artifact.py +25 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +782 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +11 -6
- alita_sdk/runtime/toolkits/tools.py +314 -70
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +24 -0
- alita_sdk/runtime/tools/application.py +16 -4
- alita_sdk/runtime/tools/artifact.py +367 -33
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +100 -4
- alita_sdk/runtime/tools/graph.py +81 -0
- alita_sdk/runtime/tools/image_generation.py +218 -0
- alita_sdk/runtime/tools/llm.py +1032 -177
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- 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 -1
- alita_sdk/runtime/tools/sandbox.py +375 -0
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +69 -65
- alita_sdk/runtime/tools/vectorstore_base.py +163 -90
- 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 +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/streamlit.py +41 -14
- alita_sdk/runtime/utils/toolkit_utils.py +28 -9
- alita_sdk/runtime/utils/utils.py +48 -0
- alita_sdk/tools/__init__.py +135 -37
- alita_sdk/tools/ado/__init__.py +2 -2
- alita_sdk/tools/ado/repos/__init__.py +16 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +27 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +28 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +28 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +14 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +28 -19
- alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
- alita_sdk/tools/browser/__init__.py +41 -16
- alita_sdk/tools/browser/crawler.py +3 -1
- alita_sdk/tools/browser/utils.py +15 -6
- alita_sdk/tools/carrier/__init__.py +18 -17
- alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
- alita_sdk/tools/carrier/excel_reporter.py +8 -4
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/codeparser.py +1 -1
- alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +12 -7
- alita_sdk/tools/cloud/azure/__init__.py +12 -7
- alita_sdk/tools/cloud/gcp/__init__.py +12 -7
- alita_sdk/tools/cloud/k8s/__init__.py +12 -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 +21 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +22 -14
- alita_sdk/tools/confluence/api_wrapper.py +197 -58
- alita_sdk/tools/confluence/loader.py +14 -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 +546 -64
- alita_sdk/tools/figma/__init__.py +60 -11
- alita_sdk/tools/figma/api_wrapper.py +1400 -167
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +18 -17
- alita_sdk/tools/github/api_wrapper.py +9 -26
- alita_sdk/tools/github/github_client.py +81 -12
- alita_sdk/tools/github/schemas.py +2 -1
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +19 -13
- alita_sdk/tools/gitlab/api_wrapper.py +256 -80
- alita_sdk/tools/gitlab_org/__init__.py +14 -10
- alita_sdk/tools/google/bigquery/__init__.py +14 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +21 -11
- alita_sdk/tools/jira/__init__.py +22 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- 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 +38 -14
- alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +491 -106
- alita_sdk/tools/openapi/api_wrapper.py +1357 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +40 -45
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/postman/api_wrapper.py +19 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +11 -10
- alita_sdk/tools/qtest/__init__.py +22 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +13 -10
- alita_sdk/tools/report_portal/__init__.py +23 -16
- alita_sdk/tools/salesforce/__init__.py +22 -16
- alita_sdk/tools/servicenow/__init__.py +21 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +17 -14
- alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +13 -8
- alita_sdk/tools/sql/__init__.py +22 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +21 -13
- alita_sdk/tools/testrail/__init__.py +13 -11
- alita_sdk/tools/testrail/api_wrapper.py +214 -46
- alita_sdk/tools/utils/__init__.py +28 -4
- alita_sdk/tools/utils/content_parser.py +241 -55
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +18 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +12 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +16 -10
- alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
- alita_sdk/tools/zephyr_essential/client.py +6 -4
- alita_sdk/tools/zephyr_scale/__init__.py +13 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +12 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
- alita_sdk-0.3.584.dist-info/RECORD +452 -0
- alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
- alita_sdk/tools/bitbucket/tools.py +0 -304
- alita_sdk-0.3.257.dist-info/RECORD +0 -343
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
alita_sdk/cli/cli.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main CLI application for Alita SDK.
|
|
3
|
+
|
|
4
|
+
Provides command-line interface for testing agents and toolkits,
|
|
5
|
+
using the same .env authentication as SDK tests and Streamlit interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Suppress warnings FIRST before any other imports
|
|
9
|
+
import warnings
|
|
10
|
+
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
11
|
+
warnings.filterwarnings('ignore', category=UserWarning)
|
|
12
|
+
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import logging
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .config import get_config
|
|
20
|
+
from .formatting import get_formatter
|
|
21
|
+
|
|
22
|
+
# Configure logging
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
level=logging.WARNING,
|
|
25
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
26
|
+
)
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group()
|
|
31
|
+
@click.option('--env-file', default='.env', help='Path to .env file')
|
|
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)')
|
|
34
|
+
@click.option('--output', type=click.Choice(['text', 'json']), default='text',
|
|
35
|
+
help='Output format')
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def cli(ctx, env_file: str, debug: bool, verbose: bool, output: str):
|
|
38
|
+
"""
|
|
39
|
+
Alita SDK CLI - Test agents and toolkits from the command line.
|
|
40
|
+
|
|
41
|
+
Credentials are loaded from .env file with variables:
|
|
42
|
+
- DEPLOYMENT_URL: Alita deployment URL
|
|
43
|
+
- PROJECT_ID: Project ID
|
|
44
|
+
- API_KEY: API authentication key
|
|
45
|
+
|
|
46
|
+
Example .env file:
|
|
47
|
+
|
|
48
|
+
DEPLOYMENT_URL=https://api.elitea.ai
|
|
49
|
+
PROJECT_ID=123
|
|
50
|
+
API_KEY=your_api_key_here
|
|
51
|
+
"""
|
|
52
|
+
ctx.ensure_object(dict)
|
|
53
|
+
|
|
54
|
+
# Enable debug logging if requested
|
|
55
|
+
if debug:
|
|
56
|
+
logging.getLogger('alita_sdk').setLevel(logging.DEBUG)
|
|
57
|
+
logger.setLevel(logging.DEBUG)
|
|
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")
|
|
64
|
+
|
|
65
|
+
# Load configuration
|
|
66
|
+
config = get_config(env_file=env_file)
|
|
67
|
+
ctx.obj['config'] = config
|
|
68
|
+
ctx.obj['formatter'] = get_formatter(output)
|
|
69
|
+
ctx.obj['debug'] = debug
|
|
70
|
+
ctx.obj['verbose'] = verbose
|
|
71
|
+
|
|
72
|
+
# Check if configuration is valid (but don't fail yet - some commands don't need it)
|
|
73
|
+
if not config.is_configured():
|
|
74
|
+
missing = config.get_missing_config()
|
|
75
|
+
ctx.obj['config_error'] = f"Missing required configuration: {', '.join(missing)}"
|
|
76
|
+
logger.debug(f"Configuration incomplete: {missing}")
|
|
77
|
+
else:
|
|
78
|
+
ctx.obj['config_error'] = None
|
|
79
|
+
logger.debug(f"Configuration loaded from {env_file}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_client(ctx):
|
|
83
|
+
"""
|
|
84
|
+
Get configured AlitaClient from context.
|
|
85
|
+
|
|
86
|
+
Raises click.ClickException if configuration is invalid.
|
|
87
|
+
"""
|
|
88
|
+
if ctx.obj.get('config_error'):
|
|
89
|
+
raise click.ClickException(
|
|
90
|
+
f"{ctx.obj['config_error']}\n\n"
|
|
91
|
+
"Please ensure your .env file contains:\n"
|
|
92
|
+
" DEPLOYMENT_URL=https://api.elitea.ai\n"
|
|
93
|
+
" PROJECT_ID=123\n"
|
|
94
|
+
" API_KEY=your_api_key_here"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Import here to avoid loading SDK if not needed
|
|
98
|
+
from alita_sdk.runtime.clients.client import AlitaClient
|
|
99
|
+
|
|
100
|
+
config = ctx.obj['config']
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
client = AlitaClient(
|
|
104
|
+
base_url=config.deployment_url,
|
|
105
|
+
project_id=config.project_id,
|
|
106
|
+
auth_token=config.api_key
|
|
107
|
+
)
|
|
108
|
+
logger.debug(f"AlitaClient initialized for project {config.project_id}")
|
|
109
|
+
return client
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise click.ClickException(f"Failed to initialize AlitaClient: {str(e)}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@cli.command()
|
|
115
|
+
@click.pass_context
|
|
116
|
+
def config(ctx):
|
|
117
|
+
"""Show current configuration (credentials masked)."""
|
|
118
|
+
config_obj = ctx.obj['config']
|
|
119
|
+
formatter = ctx.obj['formatter']
|
|
120
|
+
|
|
121
|
+
if formatter.__class__.__name__ == 'JSONFormatter':
|
|
122
|
+
click.echo(formatter._dump(config_obj.to_dict()))
|
|
123
|
+
else:
|
|
124
|
+
click.echo("\nCurrent configuration:\n")
|
|
125
|
+
for key, value in config_obj.to_dict().items():
|
|
126
|
+
click.echo(f" {key}: {value}")
|
|
127
|
+
|
|
128
|
+
if not config_obj.is_configured():
|
|
129
|
+
missing = config_obj.get_missing_config()
|
|
130
|
+
click.echo(f"\n⚠ Missing: {', '.join(missing)}")
|
|
131
|
+
else:
|
|
132
|
+
click.echo("\n✓ Configuration is complete")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Import subcommands
|
|
136
|
+
from . import toolkit
|
|
137
|
+
from . import agents
|
|
138
|
+
from . import inventory
|
|
139
|
+
|
|
140
|
+
# Register subcommands
|
|
141
|
+
cli.add_command(toolkit.toolkit)
|
|
142
|
+
cli.add_command(agents.agent)
|
|
143
|
+
cli.add_command(inventory.inventory)
|
|
144
|
+
|
|
145
|
+
# Add top-level 'chat' command as alias to 'agent chat'
|
|
146
|
+
cli.add_command(agents.agent_chat, name='chat')
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main():
|
|
150
|
+
"""Entry point for CLI."""
|
|
151
|
+
# Suppress warnings at entry point
|
|
152
|
+
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
153
|
+
warnings.filterwarnings('ignore', category=UserWarning)
|
|
154
|
+
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
cli()
|
|
158
|
+
except KeyboardInterrupt:
|
|
159
|
+
click.echo("\n\nInterrupted by user", err=True)
|
|
160
|
+
sys.exit(130)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.exception("Unexpected error")
|
|
163
|
+
click.echo(f"\nError: {str(e)}", err=True)
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == '__main__':
|
|
168
|
+
main()
|
alita_sdk/cli/config.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for Alita CLI.
|
|
3
|
+
|
|
4
|
+
Loads credentials and settings from .env files using the same pattern
|
|
5
|
+
as the SDK tests and Streamlit interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CLIConfig:
|
|
19
|
+
"""Configuration manager for Alita CLI."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, env_file: Optional[str] = None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize CLI configuration.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
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
|
|
28
|
+
"""
|
|
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'
|
|
50
|
+
self._load_env()
|
|
51
|
+
self._load_config_json()
|
|
52
|
+
|
|
53
|
+
def _load_env(self):
|
|
54
|
+
"""Load environment variables from .env file."""
|
|
55
|
+
if os.path.exists(self.env_file):
|
|
56
|
+
# Use override=True to ensure .env values take precedence
|
|
57
|
+
load_dotenv(self.env_file, override=True)
|
|
58
|
+
logger.debug(f"Loaded environment from {self.env_file}")
|
|
59
|
+
else:
|
|
60
|
+
logger.debug(f"No .env file found at {self.env_file}, using system environment")
|
|
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
|
+
|
|
119
|
+
@property
|
|
120
|
+
def deployment_url(self) -> Optional[str]:
|
|
121
|
+
"""Get deployment URL from environment or config.json."""
|
|
122
|
+
return self._get_config_value('DEPLOYMENT_URL', 'deployment_url')
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def project_id(self) -> Optional[int]:
|
|
126
|
+
"""Get project ID from environment or config.json."""
|
|
127
|
+
try:
|
|
128
|
+
value = self._get_config_value('PROJECT_ID', 'project_id')
|
|
129
|
+
return int(value) if value else None
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def api_key(self) -> Optional[str]:
|
|
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
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def alita_dir(self) -> str:
|
|
163
|
+
"""Get Alita directory from environment (defaults to .alita)."""
|
|
164
|
+
return os.getenv('ALITA_DIR', '.alita')
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def agents_dir(self) -> str:
|
|
168
|
+
"""Get agents directory (derived from ALITA_DIR)."""
|
|
169
|
+
alita_agents = os.path.join(self.alita_dir, 'agents')
|
|
170
|
+
# Fallback to .github/agents if .alita/agents doesn't exist
|
|
171
|
+
if self.alita_dir == '.alita' and not os.path.exists(alita_agents):
|
|
172
|
+
if os.path.exists('.github/agents'):
|
|
173
|
+
return '.github/agents'
|
|
174
|
+
return alita_agents
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def tools_dir(self) -> str:
|
|
178
|
+
"""Get tools directory (derived from ALITA_DIR)."""
|
|
179
|
+
return os.path.join(self.alita_dir, 'tools')
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def mcp_config_path(self) -> str:
|
|
183
|
+
"""Get MCP configuration path (derived from ALITA_DIR)."""
|
|
184
|
+
alita_mcp = os.path.join(self.alita_dir, 'mcp.json')
|
|
185
|
+
# Fallback to mcp.json in current directory
|
|
186
|
+
if not os.path.exists(alita_mcp) and os.path.exists('mcp.json'):
|
|
187
|
+
return 'mcp.json'
|
|
188
|
+
return alita_mcp
|
|
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
|
+
|
|
239
|
+
def is_configured(self) -> bool:
|
|
240
|
+
"""Check if all required configuration is present."""
|
|
241
|
+
return all([
|
|
242
|
+
self.deployment_url,
|
|
243
|
+
self.project_id is not None,
|
|
244
|
+
self.api_key
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
def get_missing_config(self) -> list[str]:
|
|
248
|
+
"""Get list of missing configuration items."""
|
|
249
|
+
missing = []
|
|
250
|
+
if not self.deployment_url:
|
|
251
|
+
missing.append('DEPLOYMENT_URL')
|
|
252
|
+
if self.project_id is None:
|
|
253
|
+
missing.append('PROJECT_ID')
|
|
254
|
+
if not self.api_key:
|
|
255
|
+
missing.append('API_KEY')
|
|
256
|
+
return missing
|
|
257
|
+
|
|
258
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
259
|
+
"""Convert configuration to dictionary."""
|
|
260
|
+
return {
|
|
261
|
+
'deployment_url': self.deployment_url,
|
|
262
|
+
'project_id': self.project_id,
|
|
263
|
+
'api_key': '***' if self.api_key else None # Masked for security
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_config(env_file: Optional[str] = None) -> CLIConfig:
|
|
268
|
+
"""
|
|
269
|
+
Get CLI configuration instance.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
env_file: Optional path to .env file
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
CLIConfig instance
|
|
276
|
+
"""
|
|
277
|
+
return CLIConfig(env_file=env_file)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def substitute_env_vars(text: str) -> str:
|
|
281
|
+
"""
|
|
282
|
+
Substitute environment variables in text.
|
|
283
|
+
|
|
284
|
+
Supports both ${VAR} and $VAR syntax.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
text: Text containing environment variable references
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Text with environment variables substituted
|
|
291
|
+
"""
|
|
292
|
+
# Replace ${VAR} syntax
|
|
293
|
+
def replace_braced(match):
|
|
294
|
+
var_name = match.group(1)
|
|
295
|
+
return os.getenv(var_name, match.group(0))
|
|
296
|
+
|
|
297
|
+
text = re.sub(r'\$\{([^}]+)\}', replace_braced, text)
|
|
298
|
+
|
|
299
|
+
# Replace $VAR syntax (word boundaries)
|
|
300
|
+
def replace_simple(match):
|
|
301
|
+
var_name = match.group(1)
|
|
302
|
+
return os.getenv(var_name, match.group(0))
|
|
303
|
+
|
|
304
|
+
text = re.sub(r'\$([A-Za-z_][A-Za-z0-9_]*)', replace_simple, text)
|
|
305
|
+
|
|
306
|
+
return text
|
|
@@ -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
|