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.

Files changed (281) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +155 -0
  6. alita_sdk/cli/agent_loader.py +215 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3794 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/toolkit.py +327 -0
  23. alita_sdk/cli/toolkit_loader.py +85 -0
  24. alita_sdk/cli/tools/__init__.py +43 -0
  25. alita_sdk/cli/tools/approval.py +224 -0
  26. alita_sdk/cli/tools/filesystem.py +1751 -0
  27. alita_sdk/cli/tools/planning.py +389 -0
  28. alita_sdk/cli/tools/terminal.py +414 -0
  29. alita_sdk/community/__init__.py +72 -12
  30. alita_sdk/community/inventory/__init__.py +236 -0
  31. alita_sdk/community/inventory/config.py +257 -0
  32. alita_sdk/community/inventory/enrichment.py +2137 -0
  33. alita_sdk/community/inventory/extractors.py +1469 -0
  34. alita_sdk/community/inventory/ingestion.py +3172 -0
  35. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  36. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  37. alita_sdk/community/inventory/parsers/base.py +295 -0
  38. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  39. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  40. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  41. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  42. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  43. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  44. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  45. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  46. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  47. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  48. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  49. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  50. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  51. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  52. alita_sdk/community/inventory/patterns/loader.py +348 -0
  53. alita_sdk/community/inventory/patterns/registry.py +198 -0
  54. alita_sdk/community/inventory/presets.py +535 -0
  55. alita_sdk/community/inventory/retrieval.py +1403 -0
  56. alita_sdk/community/inventory/toolkit.py +173 -0
  57. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  58. alita_sdk/community/inventory/visualize.py +1370 -0
  59. alita_sdk/configurations/__init__.py +11 -0
  60. alita_sdk/configurations/ado.py +148 -2
  61. alita_sdk/configurations/azure_search.py +1 -1
  62. alita_sdk/configurations/bigquery.py +1 -1
  63. alita_sdk/configurations/bitbucket.py +94 -2
  64. alita_sdk/configurations/browser.py +18 -0
  65. alita_sdk/configurations/carrier.py +19 -0
  66. alita_sdk/configurations/confluence.py +130 -1
  67. alita_sdk/configurations/delta_lake.py +1 -1
  68. alita_sdk/configurations/figma.py +76 -5
  69. alita_sdk/configurations/github.py +65 -1
  70. alita_sdk/configurations/gitlab.py +81 -0
  71. alita_sdk/configurations/google_places.py +17 -0
  72. alita_sdk/configurations/jira.py +103 -0
  73. alita_sdk/configurations/openapi.py +323 -0
  74. alita_sdk/configurations/postman.py +1 -1
  75. alita_sdk/configurations/qtest.py +72 -3
  76. alita_sdk/configurations/report_portal.py +115 -0
  77. alita_sdk/configurations/salesforce.py +19 -0
  78. alita_sdk/configurations/service_now.py +1 -12
  79. alita_sdk/configurations/sharepoint.py +167 -0
  80. alita_sdk/configurations/sonar.py +18 -0
  81. alita_sdk/configurations/sql.py +20 -0
  82. alita_sdk/configurations/testio.py +101 -0
  83. alita_sdk/configurations/testrail.py +88 -0
  84. alita_sdk/configurations/xray.py +94 -1
  85. alita_sdk/configurations/zephyr_enterprise.py +94 -1
  86. alita_sdk/configurations/zephyr_essential.py +95 -0
  87. alita_sdk/runtime/clients/artifact.py +21 -4
  88. alita_sdk/runtime/clients/client.py +458 -67
  89. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  90. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  91. alita_sdk/runtime/clients/sandbox_client.py +352 -0
  92. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  93. alita_sdk/runtime/langchain/assistant.py +183 -43
  94. alita_sdk/runtime/langchain/constants.py +647 -1
  95. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  96. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
  97. alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
  100. alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
  101. alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
  102. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
  103. alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
  104. alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
  105. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
  106. alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
  107. alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
  108. alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
  109. alita_sdk/runtime/langchain/langraph_agent.py +493 -105
  110. alita_sdk/runtime/langchain/utils.py +118 -8
  111. alita_sdk/runtime/llms/preloaded.py +2 -6
  112. alita_sdk/runtime/models/mcp_models.py +61 -0
  113. alita_sdk/runtime/skills/__init__.py +91 -0
  114. alita_sdk/runtime/skills/callbacks.py +498 -0
  115. alita_sdk/runtime/skills/discovery.py +540 -0
  116. alita_sdk/runtime/skills/executor.py +610 -0
  117. alita_sdk/runtime/skills/input_builder.py +371 -0
  118. alita_sdk/runtime/skills/models.py +330 -0
  119. alita_sdk/runtime/skills/registry.py +355 -0
  120. alita_sdk/runtime/skills/skill_runner.py +330 -0
  121. alita_sdk/runtime/toolkits/__init__.py +28 -0
  122. alita_sdk/runtime/toolkits/application.py +14 -4
  123. alita_sdk/runtime/toolkits/artifact.py +25 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +782 -0
  126. alita_sdk/runtime/toolkits/planning.py +178 -0
  127. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  128. alita_sdk/runtime/toolkits/subgraph.py +11 -6
  129. alita_sdk/runtime/toolkits/tools.py +314 -70
  130. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  131. alita_sdk/runtime/tools/__init__.py +24 -0
  132. alita_sdk/runtime/tools/application.py +16 -4
  133. alita_sdk/runtime/tools/artifact.py +367 -33
  134. alita_sdk/runtime/tools/data_analysis.py +183 -0
  135. alita_sdk/runtime/tools/function.py +100 -4
  136. alita_sdk/runtime/tools/graph.py +81 -0
  137. alita_sdk/runtime/tools/image_generation.py +218 -0
  138. alita_sdk/runtime/tools/llm.py +1032 -177
  139. alita_sdk/runtime/tools/loop.py +3 -1
  140. alita_sdk/runtime/tools/loop_output.py +3 -1
  141. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  142. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  143. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  144. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  145. alita_sdk/runtime/tools/planning/models.py +246 -0
  146. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  147. alita_sdk/runtime/tools/router.py +2 -1
  148. alita_sdk/runtime/tools/sandbox.py +375 -0
  149. alita_sdk/runtime/tools/skill_router.py +776 -0
  150. alita_sdk/runtime/tools/tool.py +3 -1
  151. alita_sdk/runtime/tools/vectorstore.py +69 -65
  152. alita_sdk/runtime/tools/vectorstore_base.py +163 -90
  153. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  154. alita_sdk/runtime/utils/constants.py +5 -1
  155. alita_sdk/runtime/utils/mcp_client.py +492 -0
  156. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  157. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  158. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  159. alita_sdk/runtime/utils/streamlit.py +41 -14
  160. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  161. alita_sdk/runtime/utils/utils.py +48 -0
  162. alita_sdk/tools/__init__.py +135 -37
  163. alita_sdk/tools/ado/__init__.py +2 -2
  164. alita_sdk/tools/ado/repos/__init__.py +16 -19
  165. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  166. alita_sdk/tools/ado/test_plan/__init__.py +27 -8
  167. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  168. alita_sdk/tools/ado/wiki/__init__.py +28 -12
  169. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  170. alita_sdk/tools/ado/work_item/__init__.py +28 -12
  171. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  172. alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
  173. alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
  174. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  175. alita_sdk/tools/azure_ai/search/__init__.py +14 -8
  176. alita_sdk/tools/base/tool.py +5 -1
  177. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  178. alita_sdk/tools/bitbucket/__init__.py +28 -19
  179. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  180. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  181. alita_sdk/tools/browser/__init__.py +41 -16
  182. alita_sdk/tools/browser/crawler.py +3 -1
  183. alita_sdk/tools/browser/utils.py +15 -6
  184. alita_sdk/tools/carrier/__init__.py +18 -17
  185. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  186. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  187. alita_sdk/tools/chunkers/__init__.py +3 -1
  188. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  189. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  190. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  191. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  192. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  193. alita_sdk/tools/cloud/aws/__init__.py +12 -7
  194. alita_sdk/tools/cloud/azure/__init__.py +12 -7
  195. alita_sdk/tools/cloud/gcp/__init__.py +12 -7
  196. alita_sdk/tools/cloud/k8s/__init__.py +12 -7
  197. alita_sdk/tools/code/linter/__init__.py +10 -8
  198. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  199. alita_sdk/tools/code/sonar/__init__.py +21 -13
  200. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  201. alita_sdk/tools/confluence/__init__.py +22 -14
  202. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  203. alita_sdk/tools/confluence/loader.py +14 -2
  204. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  205. alita_sdk/tools/elastic/__init__.py +11 -8
  206. alita_sdk/tools/elitea_base.py +546 -64
  207. alita_sdk/tools/figma/__init__.py +60 -11
  208. alita_sdk/tools/figma/api_wrapper.py +1400 -167
  209. alita_sdk/tools/figma/figma_client.py +73 -0
  210. alita_sdk/tools/figma/toon_tools.py +2748 -0
  211. alita_sdk/tools/github/__init__.py +18 -17
  212. alita_sdk/tools/github/api_wrapper.py +9 -26
  213. alita_sdk/tools/github/github_client.py +81 -12
  214. alita_sdk/tools/github/schemas.py +2 -1
  215. alita_sdk/tools/github/tool.py +5 -1
  216. alita_sdk/tools/gitlab/__init__.py +19 -13
  217. alita_sdk/tools/gitlab/api_wrapper.py +256 -80
  218. alita_sdk/tools/gitlab_org/__init__.py +14 -10
  219. alita_sdk/tools/google/bigquery/__init__.py +14 -13
  220. alita_sdk/tools/google/bigquery/tool.py +5 -1
  221. alita_sdk/tools/google_places/__init__.py +21 -11
  222. alita_sdk/tools/jira/__init__.py +22 -11
  223. alita_sdk/tools/jira/api_wrapper.py +315 -168
  224. alita_sdk/tools/keycloak/__init__.py +11 -8
  225. alita_sdk/tools/localgit/__init__.py +9 -3
  226. alita_sdk/tools/localgit/local_git.py +62 -54
  227. alita_sdk/tools/localgit/tool.py +5 -1
  228. alita_sdk/tools/memory/__init__.py +38 -14
  229. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  230. alita_sdk/tools/ocr/__init__.py +11 -8
  231. alita_sdk/tools/openapi/__init__.py +491 -106
  232. alita_sdk/tools/openapi/api_wrapper.py +1357 -0
  233. alita_sdk/tools/openapi/tool.py +20 -0
  234. alita_sdk/tools/pandas/__init__.py +20 -12
  235. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  236. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  237. alita_sdk/tools/postman/__init__.py +11 -11
  238. alita_sdk/tools/postman/api_wrapper.py +19 -8
  239. alita_sdk/tools/postman/postman_analysis.py +8 -1
  240. alita_sdk/tools/pptx/__init__.py +11 -10
  241. alita_sdk/tools/qtest/__init__.py +22 -14
  242. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  243. alita_sdk/tools/rally/__init__.py +13 -10
  244. alita_sdk/tools/report_portal/__init__.py +23 -16
  245. alita_sdk/tools/salesforce/__init__.py +22 -16
  246. alita_sdk/tools/servicenow/__init__.py +21 -16
  247. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  248. alita_sdk/tools/sharepoint/__init__.py +17 -14
  249. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  250. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  251. alita_sdk/tools/sharepoint/utils.py +8 -2
  252. alita_sdk/tools/slack/__init__.py +13 -8
  253. alita_sdk/tools/sql/__init__.py +22 -19
  254. alita_sdk/tools/sql/api_wrapper.py +71 -23
  255. alita_sdk/tools/testio/__init__.py +21 -13
  256. alita_sdk/tools/testrail/__init__.py +13 -11
  257. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  258. alita_sdk/tools/utils/__init__.py +28 -4
  259. alita_sdk/tools/utils/content_parser.py +241 -55
  260. alita_sdk/tools/utils/text_operations.py +254 -0
  261. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  262. alita_sdk/tools/xray/__init__.py +18 -14
  263. alita_sdk/tools/xray/api_wrapper.py +58 -113
  264. alita_sdk/tools/yagmail/__init__.py +9 -3
  265. alita_sdk/tools/zephyr/__init__.py +12 -7
  266. alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
  267. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  268. alita_sdk/tools/zephyr_essential/__init__.py +16 -10
  269. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  270. alita_sdk/tools/zephyr_essential/client.py +6 -4
  271. alita_sdk/tools/zephyr_scale/__init__.py +13 -8
  272. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  273. alita_sdk/tools/zephyr_squad/__init__.py +12 -7
  274. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
  275. alita_sdk-0.3.584.dist-info/RECORD +452 -0
  276. alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
  277. alita_sdk/tools/bitbucket/tools.py +0 -304
  278. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  279. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
  280. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
  281. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,731 @@
1
+ """
2
+ CLI Context Manager for chat history management.
3
+
4
+ Provides token-aware context management with pruning, summarization,
5
+ and efficient tracking of message inclusion status.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, List, Optional
15
+
16
+ from .message import CLIMessage, cli_messages_to_dicts, cli_messages_to_langchain
17
+ from .token_estimation import estimate_tokens, calculate_total_tokens
18
+ from .strategies import PruningOrchestrator, PruningConfig
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class ContextInfo:
25
+ """
26
+ Information about current context state for UI display.
27
+
28
+ Attributes:
29
+ used_tokens: Tokens currently in context
30
+ max_tokens: Maximum allowed tokens
31
+ fill_ratio: Percentage of context used (0.0-1.0)
32
+ message_count: Total messages in history
33
+ included_count: Messages included in context
34
+ pruned_count: Messages excluded from context
35
+ summary_count: Number of active summaries
36
+ """
37
+ used_tokens: int = 0
38
+ max_tokens: int = 8000
39
+ fill_ratio: float = 0.0
40
+ message_count: int = 0
41
+ included_count: int = 0
42
+ pruned_count: int = 0
43
+ summary_count: int = 0
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ """Convert to dictionary for serialization."""
47
+ return {
48
+ 'used_tokens': self.used_tokens,
49
+ 'max_tokens': self.max_tokens,
50
+ 'fill_ratio': self.fill_ratio,
51
+ 'message_count': self.message_count,
52
+ 'included_count': self.included_count,
53
+ 'pruned_count': self.pruned_count,
54
+ 'summary_count': self.summary_count,
55
+ }
56
+
57
+
58
+ @dataclass
59
+ class Summary:
60
+ """
61
+ Conversation summary for context compression.
62
+
63
+ Attributes:
64
+ content: Summary text
65
+ from_idx: Start message index (inclusive)
66
+ to_idx: End message index (inclusive)
67
+ token_count: Token count of summary
68
+ created_at: Creation timestamp
69
+ """
70
+ content: str
71
+ from_idx: int
72
+ to_idx: int
73
+ token_count: int = 0
74
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
75
+
76
+ def __post_init__(self):
77
+ """Calculate token count if not provided."""
78
+ if self.token_count == 0 and self.content:
79
+ self.token_count = estimate_tokens(self.content)
80
+
81
+ def to_dict(self) -> Dict[str, Any]:
82
+ """Convert to dictionary for serialization."""
83
+ return {
84
+ 'content': self.content,
85
+ 'from_idx': self.from_idx,
86
+ 'to_idx': self.to_idx,
87
+ 'token_count': self.token_count,
88
+ 'created_at': self.created_at.isoformat(),
89
+ }
90
+
91
+ @classmethod
92
+ def from_dict(cls, data: Dict[str, Any]) -> 'Summary':
93
+ """Create Summary from dictionary."""
94
+ created_at = data.get('created_at')
95
+ if isinstance(created_at, str):
96
+ created_at = datetime.fromisoformat(created_at)
97
+ elif created_at is None:
98
+ created_at = datetime.now(timezone.utc)
99
+
100
+ return cls(
101
+ content=data['content'],
102
+ from_idx=data['from_idx'],
103
+ to_idx=data['to_idx'],
104
+ token_count=data.get('token_count', 0),
105
+ created_at=created_at,
106
+ )
107
+
108
+
109
+ class CLIContextManager:
110
+ """
111
+ Manages chat history context with token-aware pruning and summarization.
112
+
113
+ Features:
114
+ - Incremental token tracking (O(1) for new messages)
115
+ - Lazy pruning (only when needed)
116
+ - In-memory message inclusion tracking
117
+ - Summary generation and management
118
+ - Session state persistence
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ config: Optional[Dict[str, Any]] = None,
124
+ session_id: Optional[str] = None,
125
+ alita_dir: str = '.alita',
126
+ # Convenience parameters (override config if provided)
127
+ max_context_tokens: Optional[int] = None,
128
+ preserve_recent: Optional[int] = None,
129
+ pruning_method: Optional[str] = None,
130
+ enable_summarization: Optional[bool] = None,
131
+ summary_trigger_ratio: Optional[float] = None,
132
+ summaries_limit: Optional[int] = None,
133
+ llm: Optional[Any] = None,
134
+ ):
135
+ """
136
+ Initialize context manager.
137
+
138
+ Args:
139
+ config: Context management configuration dict
140
+ session_id: Session ID for persistence
141
+ alita_dir: Base Alita directory
142
+ max_context_tokens: Maximum tokens in context (overrides config)
143
+ preserve_recent: Number of recent messages to preserve (overrides config)
144
+ pruning_method: Pruning strategy name (overrides config)
145
+ enable_summarization: Enable automatic summarization (overrides config)
146
+ summary_trigger_ratio: Context fill ratio that triggers summarization (overrides config)
147
+ summaries_limit: Maximum number of summaries to keep (overrides config)
148
+ llm: LLM instance for summarization
149
+ """
150
+ # Default configuration
151
+ self.config = {
152
+ 'enabled': True,
153
+ 'max_context_tokens': 8000,
154
+ 'preserve_recent_messages': 5,
155
+ 'pruning_method': 'oldest_first',
156
+ 'enable_summarization': True,
157
+ 'summary_trigger_ratio': 0.8,
158
+ 'summaries_limit_count': 5,
159
+ 'weights': {
160
+ 'recency': 1.0,
161
+ 'importance': 1.0,
162
+ 'user_messages': 1.2,
163
+ 'thread_continuity': 1.0,
164
+ },
165
+ }
166
+ if config:
167
+ self.config.update(config)
168
+
169
+ # Apply convenience parameters
170
+ if max_context_tokens is not None:
171
+ self.config['max_context_tokens'] = max_context_tokens
172
+ if preserve_recent is not None:
173
+ self.config['preserve_recent_messages'] = preserve_recent
174
+ if pruning_method is not None:
175
+ self.config['pruning_method'] = pruning_method
176
+ if enable_summarization is not None:
177
+ self.config['enable_summarization'] = enable_summarization
178
+ if summary_trigger_ratio is not None:
179
+ self.config['summary_trigger_ratio'] = summary_trigger_ratio
180
+ if summaries_limit is not None:
181
+ self.config['summaries_limit_count'] = summaries_limit
182
+
183
+ self.session_id = session_id
184
+ self.alita_dir = alita_dir
185
+ self.llm = llm # LLM for summarization
186
+
187
+ # Message storage
188
+ self._messages: List[CLIMessage] = []
189
+ self._summaries: List[Summary] = []
190
+
191
+ # Token tracking (incremental)
192
+ self._included_tokens: int = 0
193
+ self._summary_tokens: int = 0
194
+ self._needs_pruning: bool = False
195
+
196
+ # Pruning orchestrator
197
+ self._orchestrator = PruningOrchestrator(self.config)
198
+
199
+ # Load existing state if session provided
200
+ if session_id:
201
+ self._load_state()
202
+
203
+ @property
204
+ def max_tokens(self) -> int:
205
+ """Get maximum context tokens."""
206
+ return self.config.get('max_context_tokens', 8000)
207
+
208
+ @property
209
+ def total_tokens(self) -> int:
210
+ """Get total tokens in context (messages + summaries)."""
211
+ return self._included_tokens + self._summary_tokens
212
+
213
+ @property
214
+ def fill_ratio(self) -> float:
215
+ """Get context fill ratio (0.0-1.0)."""
216
+ if self.max_tokens <= 0:
217
+ return 0.0
218
+ return min(1.0, self.total_tokens / self.max_tokens)
219
+
220
+ def add_message(self, role: str, content: str) -> CLIMessage:
221
+ """
222
+ Add a new message to the history.
223
+
224
+ Args:
225
+ role: Message role (user, assistant, system)
226
+ content: Message content
227
+
228
+ Returns:
229
+ The created CLIMessage
230
+ """
231
+ index = len(self._messages)
232
+ message = CLIMessage(
233
+ role=role,
234
+ content=content,
235
+ index=index,
236
+ )
237
+
238
+ self._messages.append(message)
239
+ self._included_tokens += message.token_count
240
+
241
+ # Check if we need to prune
242
+ if self.total_tokens > self.max_tokens:
243
+ self._needs_pruning = True
244
+
245
+ return message
246
+
247
+ def build_context(
248
+ self,
249
+ system_prompt: Optional[str] = None,
250
+ force_prune: bool = False,
251
+ ) -> List[Dict[str, str]]:
252
+ """
253
+ Build optimized context for LLM invocation.
254
+
255
+ This is the main method called before each LLM call.
256
+ Only performs pruning if tokens exceed limit.
257
+
258
+ Args:
259
+ system_prompt: Optional system prompt to include
260
+ force_prune: Force re-pruning even if not needed
261
+
262
+ Returns:
263
+ List of message dicts with 'role' and 'content' keys
264
+ """
265
+ if not self.config.get('enabled', True):
266
+ # Context management disabled, return all messages
267
+ messages = cli_messages_to_dicts(self._messages, include_only=False)
268
+ if system_prompt:
269
+ messages.insert(0, {'role': 'system', 'content': system_prompt})
270
+
271
+ return messages
272
+
273
+ # Prune if needed
274
+ if self._needs_pruning or force_prune:
275
+ self._apply_pruning()
276
+
277
+ # Build message list
278
+ messages = []
279
+
280
+ # Add system prompt first
281
+ if system_prompt:
282
+ messages.append({'role': 'system', 'content': system_prompt})
283
+
284
+ # Add summaries as system context
285
+ if self._summaries:
286
+ summary_text = self._format_summaries_for_context()
287
+ if summary_text:
288
+ messages.append({
289
+ 'role': 'system',
290
+ 'content': f"Previous conversation summary:\n{summary_text}"
291
+ })
292
+
293
+ # Add included messages
294
+ messages.extend(cli_messages_to_dicts(self._messages, include_only=True))
295
+
296
+ return messages
297
+
298
+ def build_context_langchain(
299
+ self,
300
+ system_prompt: Optional[str] = None,
301
+ force_prune: bool = False,
302
+ ) -> List[Any]:
303
+ """
304
+ Build optimized context as LangChain messages.
305
+
306
+ Args:
307
+ system_prompt: Optional system prompt to include
308
+ force_prune: Force re-pruning even if not needed
309
+
310
+ Returns:
311
+ List of LangChain message objects
312
+ """
313
+ from langchain_core.messages import SystemMessage
314
+
315
+ if not self.config.get('enabled', True):
316
+ messages = cli_messages_to_langchain(self._messages, include_only=False)
317
+ if system_prompt:
318
+ messages.insert(0, SystemMessage(content=system_prompt))
319
+ return messages
320
+
321
+ if self._needs_pruning or force_prune:
322
+ self._apply_pruning()
323
+
324
+ messages = []
325
+
326
+ if system_prompt:
327
+ messages.append(SystemMessage(content=system_prompt))
328
+
329
+ if self._summaries:
330
+ summary_text = self._format_summaries_for_context()
331
+ if summary_text:
332
+ messages.append(SystemMessage(
333
+ content=f"Previous conversation summary:\n{summary_text}"
334
+ ))
335
+
336
+ messages.extend(cli_messages_to_langchain(self._messages, include_only=True))
337
+
338
+ return messages
339
+
340
+ def _apply_pruning(self):
341
+ """Apply pruning strategy to reduce context size."""
342
+ if not self._messages:
343
+ self._needs_pruning = False
344
+ return
345
+
346
+ # Apply pruning
347
+ self._orchestrator.apply_pruning(
348
+ self._messages,
349
+ summary_tokens=self._summary_tokens,
350
+ )
351
+
352
+ # Recalculate included tokens
353
+ self._included_tokens = sum(
354
+ m.token_count for m in self._messages if m.included
355
+ )
356
+
357
+ self._needs_pruning = False
358
+
359
+ # Check if we should trigger summarization
360
+ if self.config.get('enable_summarization', True):
361
+ self._maybe_trigger_summarization()
362
+
363
+ # Save state if session exists
364
+ if self.session_id:
365
+ self._save_state()
366
+
367
+ def _maybe_trigger_summarization(self):
368
+ """Check if summarization should be triggered and mark for it."""
369
+ trigger_ratio = self.config.get('summary_trigger_ratio', 0.8)
370
+
371
+ if self.fill_ratio < trigger_ratio:
372
+ return
373
+
374
+ # Count pruned messages that haven't been summarized
375
+ pruned_messages = [m for m in self._messages if not m.included]
376
+
377
+ # Check if we have enough pruned messages to summarize
378
+ min_for_summary = 3
379
+ if len(pruned_messages) >= min_for_summary:
380
+ # Find the range of pruned messages
381
+ if pruned_messages:
382
+ from_idx = min(m.index for m in pruned_messages)
383
+ to_idx = max(m.index for m in pruned_messages)
384
+
385
+ # Check if this range is already summarized
386
+ for summary in self._summaries:
387
+ if summary.from_idx <= from_idx and summary.to_idx >= to_idx:
388
+ return # Already summarized
389
+
390
+ logger.debug(
391
+ f"Summarization needed: {len(pruned_messages)} pruned messages "
392
+ f"(indices {from_idx}-{to_idx})"
393
+ )
394
+
395
+ def generate_summary(
396
+ self,
397
+ llm: Any,
398
+ from_idx: Optional[int] = None,
399
+ to_idx: Optional[int] = None,
400
+ ) -> Optional[Summary]:
401
+ """
402
+ Generate a summary of pruned messages using the LLM.
403
+
404
+ Args:
405
+ llm: LLM instance with invoke() method
406
+ from_idx: Start message index (default: first pruned)
407
+ to_idx: End message index (default: last pruned)
408
+
409
+ Returns:
410
+ Generated Summary or None if failed/not needed
411
+ """
412
+ # Find pruned messages to summarize
413
+ pruned_messages = [m for m in self._messages if not m.included]
414
+
415
+ if not pruned_messages:
416
+ return None
417
+
418
+ if from_idx is None:
419
+ from_idx = min(m.index for m in pruned_messages)
420
+ if to_idx is None:
421
+ to_idx = max(m.index for m in pruned_messages)
422
+
423
+ # Get messages in range
424
+ messages_to_summarize = [
425
+ m for m in self._messages
426
+ if from_idx <= m.index <= to_idx
427
+ ]
428
+
429
+ if len(messages_to_summarize) < 3:
430
+ return None
431
+
432
+ # Build summary prompt
433
+ conversation_text = self._format_messages_for_summary(messages_to_summarize)
434
+
435
+ summary_instructions = self.config.get('summary_instructions') or (
436
+ "Summarize the following conversation concisely, preserving key information, "
437
+ "decisions made, and important context. Focus on facts and outcomes."
438
+ )
439
+
440
+ prompt = f"{summary_instructions}\n\nConversation:\n{conversation_text}"
441
+
442
+ try:
443
+ response = llm.invoke([{'role': 'user', 'content': prompt}])
444
+ summary_content = response.content if hasattr(response, 'content') else str(response)
445
+
446
+ summary = Summary(
447
+ content=summary_content,
448
+ from_idx=from_idx,
449
+ to_idx=to_idx,
450
+ )
451
+
452
+ # Add summary and update tokens
453
+ self._summaries.append(summary)
454
+ self._summary_tokens += summary.token_count
455
+
456
+ # Prune old summaries if limit exceeded
457
+ self._prune_old_summaries()
458
+
459
+ # Save state
460
+ if self.session_id:
461
+ self._save_state()
462
+
463
+ logger.info(f"Generated summary for messages {from_idx}-{to_idx}")
464
+ return summary
465
+
466
+ except Exception as e:
467
+ logger.warning(f"Failed to generate summary: {e}")
468
+ return None
469
+
470
+ def _prune_old_summaries(self):
471
+ """Remove oldest summaries if limit exceeded."""
472
+ limit = self.config.get('summaries_limit_count', 5)
473
+
474
+ while len(self._summaries) > limit:
475
+ removed = self._summaries.pop(0)
476
+ self._summary_tokens -= removed.token_count
477
+ logger.debug(f"Pruned old summary (indices {removed.from_idx}-{removed.to_idx})")
478
+
479
+ def _format_messages_for_summary(self, messages: List[CLIMessage]) -> str:
480
+ """Format messages for summary generation prompt."""
481
+ lines = []
482
+ for msg in messages:
483
+ role = msg.role.capitalize()
484
+ lines.append(f"{role}: {msg.content}")
485
+ return "\n\n".join(lines)
486
+
487
+ def _format_summaries_for_context(self) -> str:
488
+ """Format all summaries for inclusion in context."""
489
+ if not self._summaries:
490
+ return ""
491
+
492
+ parts = []
493
+ for i, summary in enumerate(self._summaries, 1):
494
+ if len(self._summaries) > 1:
495
+ parts.append(f"[Part {i}] {summary.content}")
496
+ else:
497
+ parts.append(summary.content)
498
+
499
+ return "\n\n".join(parts)
500
+
501
+ def _build_context_info(self) -> ContextInfo:
502
+ """Build context info for UI display."""
503
+ included_count = sum(1 for m in self._messages if m.included)
504
+
505
+ return ContextInfo(
506
+ used_tokens=self.total_tokens,
507
+ max_tokens=self.max_tokens,
508
+ fill_ratio=self.fill_ratio,
509
+ message_count=len(self._messages),
510
+ included_count=included_count,
511
+ pruned_count=len(self._messages) - included_count,
512
+ summary_count=len(self._summaries),
513
+ )
514
+
515
+ def get_context_info(self) -> Dict[str, Any]:
516
+ """
517
+ Get current context info as a dictionary for UI display.
518
+
519
+ Returns:
520
+ Dict with keys: used_tokens, max_tokens, fill_ratio, pruned_count, etc.
521
+ """
522
+ return self._build_context_info().to_dict()
523
+
524
+ def is_message_included(self, index: int) -> bool:
525
+ """
526
+ Check if a message at a given index is included in context.
527
+
528
+ Args:
529
+ index: Message index (0-based)
530
+
531
+ Returns:
532
+ True if included, False if pruned
533
+ """
534
+ if 0 <= index < len(self._messages):
535
+ return self._messages[index].included
536
+ return False
537
+
538
+ def clear(self):
539
+ """Clear all messages and summaries."""
540
+ self._messages.clear()
541
+ self._summaries.clear()
542
+ self._included_tokens = 0
543
+ self._summary_tokens = 0
544
+ self._needs_pruning = False
545
+
546
+ if self.session_id:
547
+ self._save_state()
548
+
549
+ def _get_state_path(self) -> Path:
550
+ """Get path to context state file."""
551
+ if self.alita_dir.startswith('~'):
552
+ base = os.path.expanduser(self.alita_dir)
553
+ else:
554
+ base = self.alita_dir
555
+
556
+ return Path(base) / 'sessions' / self.session_id / 'context_state.json'
557
+
558
+ def _save_state(self):
559
+ """Save context state to disk."""
560
+ if not self.session_id:
561
+ return
562
+
563
+ state_path = self._get_state_path()
564
+ state_path.parent.mkdir(parents=True, exist_ok=True)
565
+
566
+ state = {
567
+ 'messages': [m.to_state_dict() for m in self._messages],
568
+ 'summaries': [s.to_dict() for s in self._summaries],
569
+ 'included_tokens': self._included_tokens,
570
+ 'summary_tokens': self._summary_tokens,
571
+ 'saved_at': datetime.now(timezone.utc).isoformat(),
572
+ }
573
+
574
+ try:
575
+ with open(state_path, 'w') as f:
576
+ json.dump(state, f, indent=2)
577
+ logger.debug(f"Saved context state to {state_path}")
578
+ except IOError as e:
579
+ logger.warning(f"Failed to save context state: {e}")
580
+
581
+ def _load_state(self):
582
+ """Load context state from disk."""
583
+ if not self.session_id:
584
+ return
585
+
586
+ state_path = self._get_state_path()
587
+
588
+ if not state_path.exists():
589
+ return
590
+
591
+ try:
592
+ with open(state_path, 'r') as f:
593
+ state = json.load(f)
594
+
595
+ self._messages = [
596
+ CLIMessage.from_state_dict(m)
597
+ for m in state.get('messages', [])
598
+ ]
599
+ self._summaries = [
600
+ Summary.from_dict(s)
601
+ for s in state.get('summaries', [])
602
+ ]
603
+ self._included_tokens = state.get('included_tokens', 0)
604
+ self._summary_tokens = state.get('summary_tokens', 0)
605
+
606
+ # Recalculate if needed
607
+ if self._included_tokens == 0 and self._messages:
608
+ self._included_tokens = sum(
609
+ m.token_count for m in self._messages if m.included
610
+ )
611
+ if self._summary_tokens == 0 and self._summaries:
612
+ self._summary_tokens = sum(s.token_count for s in self._summaries)
613
+
614
+ logger.debug(f"Loaded context state from {state_path}")
615
+
616
+ except (IOError, json.JSONDecodeError) as e:
617
+ logger.warning(f"Failed to load context state: {e}")
618
+
619
+ def import_chat_history(self, chat_history: List[Dict[str, str]]):
620
+ """
621
+ Import existing chat history into the context manager.
622
+
623
+ Useful for migrating existing conversations or session resume.
624
+
625
+ Args:
626
+ chat_history: List of message dicts with 'role' and 'content'
627
+ """
628
+ for msg_dict in chat_history:
629
+ role = msg_dict.get('role', 'user')
630
+ content = msg_dict.get('content', '')
631
+ if content:
632
+ self.add_message(role, content)
633
+
634
+ def export_chat_history(self, include_only: bool = False) -> List[Dict[str, str]]:
635
+ """
636
+ Export chat history as list of message dicts.
637
+
638
+ Args:
639
+ include_only: If True, only export included messages
640
+
641
+ Returns:
642
+ List of message dicts with 'role' and 'content'
643
+ """
644
+ return cli_messages_to_dicts(self._messages, include_only=include_only)
645
+
646
+
647
+ def sanitize_message_history(messages: List[Any]) -> List[Any]:
648
+ """
649
+ Sanitize message history to ensure valid tool call/response structure.
650
+
651
+ This function ensures that any AIMessage with tool_calls has corresponding
652
+ ToolMessages for all tool_call_ids. This prevents the LLM API error:
653
+ "An assistant message with 'tool_calls' must be followed by tool messages
654
+ responding to each 'tool_call_id'."
655
+
656
+ Use this when:
657
+ - Resuming from a GraphRecursionError (step limit)
658
+ - Resuming from a tool execution limit
659
+ - Loading corrupted checkpoint state
660
+
661
+ Args:
662
+ messages: List of LangChain message objects or dicts
663
+
664
+ Returns:
665
+ Sanitized list of messages with placeholder ToolMessages added for any
666
+ missing tool call responses.
667
+ """
668
+ from langchain_core.messages import ToolMessage, AIMessage as LCAIMessage
669
+
670
+ if not messages:
671
+ return messages
672
+
673
+ result = list(messages) # Copy to avoid mutating original
674
+
675
+ # Build set of existing tool_call_ids that have responses
676
+ existing_tool_responses = set()
677
+ for msg in result:
678
+ if hasattr(msg, 'tool_call_id') and msg.tool_call_id:
679
+ existing_tool_responses.add(msg.tool_call_id)
680
+ elif isinstance(msg, dict) and msg.get('type') == 'tool':
681
+ tool_call_id = msg.get('tool_call_id')
682
+ if tool_call_id:
683
+ existing_tool_responses.add(tool_call_id)
684
+
685
+ # Find AIMessages with tool_calls and check for missing responses
686
+ messages_to_add = []
687
+ for i, msg in enumerate(result):
688
+ tool_calls = None
689
+
690
+ # Check for tool_calls in different message formats
691
+ if hasattr(msg, 'tool_calls') and msg.tool_calls:
692
+ tool_calls = msg.tool_calls
693
+ elif isinstance(msg, dict) and msg.get('tool_calls'):
694
+ tool_calls = msg.get('tool_calls')
695
+
696
+ if tool_calls:
697
+ # Check each tool_call for a corresponding response
698
+ for tool_call in tool_calls:
699
+ tool_call_id = None
700
+ tool_name = 'unknown'
701
+
702
+ if isinstance(tool_call, dict):
703
+ tool_call_id = tool_call.get('id', '')
704
+ tool_name = tool_call.get('name', 'unknown')
705
+ elif hasattr(tool_call, 'id'):
706
+ tool_call_id = getattr(tool_call, 'id', '')
707
+ tool_name = getattr(tool_call, 'name', 'unknown')
708
+
709
+ if tool_call_id and tool_call_id not in existing_tool_responses:
710
+ # Missing tool response - create placeholder
711
+ logger.warning(
712
+ f"Found AIMessage with tool_call '{tool_name}' ({tool_call_id}) "
713
+ f"without corresponding ToolMessage. Adding placeholder."
714
+ )
715
+ placeholder = ToolMessage(
716
+ content=f"[Tool call '{tool_name}' was interrupted - no response available. "
717
+ f"The task may need to be retried.]",
718
+ tool_call_id=tool_call_id
719
+ )
720
+ messages_to_add.append((i + 1, placeholder))
721
+ existing_tool_responses.add(tool_call_id) # Prevent duplicates
722
+
723
+ # Insert placeholder messages (reverse order to maintain correct indices)
724
+ for insert_idx, placeholder_msg in reversed(messages_to_add):
725
+ result.insert(insert_idx, placeholder_msg)
726
+
727
+ if messages_to_add:
728
+ logger.info(f"Sanitized message history: added {len(messages_to_add)} placeholder ToolMessages")
729
+
730
+ return result
731
+