alita-sdk 0.3.257__py3-none-any.whl → 0.3.562__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +155 -0
  6. alita_sdk/cli/agent_loader.py +215 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3601 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/toolkit.py +327 -0
  23. alita_sdk/cli/toolkit_loader.py +85 -0
  24. alita_sdk/cli/tools/__init__.py +43 -0
  25. alita_sdk/cli/tools/approval.py +224 -0
  26. alita_sdk/cli/tools/filesystem.py +1751 -0
  27. alita_sdk/cli/tools/planning.py +389 -0
  28. alita_sdk/cli/tools/terminal.py +414 -0
  29. alita_sdk/community/__init__.py +72 -12
  30. alita_sdk/community/inventory/__init__.py +236 -0
  31. alita_sdk/community/inventory/config.py +257 -0
  32. alita_sdk/community/inventory/enrichment.py +2137 -0
  33. alita_sdk/community/inventory/extractors.py +1469 -0
  34. alita_sdk/community/inventory/ingestion.py +3172 -0
  35. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  36. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  37. alita_sdk/community/inventory/parsers/base.py +295 -0
  38. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  39. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  40. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  41. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  42. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  43. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  44. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  45. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  46. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  47. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  48. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  49. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  50. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  51. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  52. alita_sdk/community/inventory/patterns/loader.py +348 -0
  53. alita_sdk/community/inventory/patterns/registry.py +198 -0
  54. alita_sdk/community/inventory/presets.py +535 -0
  55. alita_sdk/community/inventory/retrieval.py +1403 -0
  56. alita_sdk/community/inventory/toolkit.py +173 -0
  57. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  58. alita_sdk/community/inventory/visualize.py +1370 -0
  59. alita_sdk/configurations/__init__.py +11 -0
  60. alita_sdk/configurations/ado.py +148 -2
  61. alita_sdk/configurations/azure_search.py +1 -1
  62. alita_sdk/configurations/bigquery.py +1 -1
  63. alita_sdk/configurations/bitbucket.py +94 -2
  64. alita_sdk/configurations/browser.py +18 -0
  65. alita_sdk/configurations/carrier.py +19 -0
  66. alita_sdk/configurations/confluence.py +130 -1
  67. alita_sdk/configurations/delta_lake.py +1 -1
  68. alita_sdk/configurations/figma.py +76 -5
  69. alita_sdk/configurations/github.py +65 -1
  70. alita_sdk/configurations/gitlab.py +81 -0
  71. alita_sdk/configurations/google_places.py +17 -0
  72. alita_sdk/configurations/jira.py +103 -0
  73. alita_sdk/configurations/openapi.py +111 -0
  74. alita_sdk/configurations/postman.py +1 -1
  75. alita_sdk/configurations/qtest.py +72 -3
  76. alita_sdk/configurations/report_portal.py +115 -0
  77. alita_sdk/configurations/salesforce.py +19 -0
  78. alita_sdk/configurations/service_now.py +1 -12
  79. alita_sdk/configurations/sharepoint.py +167 -0
  80. alita_sdk/configurations/sonar.py +18 -0
  81. alita_sdk/configurations/sql.py +20 -0
  82. alita_sdk/configurations/testio.py +101 -0
  83. alita_sdk/configurations/testrail.py +88 -0
  84. alita_sdk/configurations/xray.py +94 -1
  85. alita_sdk/configurations/zephyr_enterprise.py +94 -1
  86. alita_sdk/configurations/zephyr_essential.py +95 -0
  87. alita_sdk/runtime/clients/artifact.py +21 -4
  88. alita_sdk/runtime/clients/client.py +458 -67
  89. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  90. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  91. alita_sdk/runtime/clients/sandbox_client.py +352 -0
  92. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  93. alita_sdk/runtime/langchain/assistant.py +183 -43
  94. alita_sdk/runtime/langchain/constants.py +647 -1
  95. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  96. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
  97. alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
  100. alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
  101. alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
  102. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
  103. alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
  104. alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
  105. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
  106. alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
  107. alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
  108. alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
  109. alita_sdk/runtime/langchain/langraph_agent.py +407 -92
  110. alita_sdk/runtime/langchain/utils.py +102 -8
  111. alita_sdk/runtime/llms/preloaded.py +2 -6
  112. alita_sdk/runtime/models/mcp_models.py +61 -0
  113. alita_sdk/runtime/skills/__init__.py +91 -0
  114. alita_sdk/runtime/skills/callbacks.py +498 -0
  115. alita_sdk/runtime/skills/discovery.py +540 -0
  116. alita_sdk/runtime/skills/executor.py +610 -0
  117. alita_sdk/runtime/skills/input_builder.py +371 -0
  118. alita_sdk/runtime/skills/models.py +330 -0
  119. alita_sdk/runtime/skills/registry.py +355 -0
  120. alita_sdk/runtime/skills/skill_runner.py +330 -0
  121. alita_sdk/runtime/toolkits/__init__.py +28 -0
  122. alita_sdk/runtime/toolkits/application.py +14 -4
  123. alita_sdk/runtime/toolkits/artifact.py +24 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +780 -0
  126. alita_sdk/runtime/toolkits/planning.py +178 -0
  127. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  128. alita_sdk/runtime/toolkits/subgraph.py +11 -6
  129. alita_sdk/runtime/toolkits/tools.py +314 -70
  130. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  131. alita_sdk/runtime/tools/__init__.py +24 -0
  132. alita_sdk/runtime/tools/application.py +16 -4
  133. alita_sdk/runtime/tools/artifact.py +367 -33
  134. alita_sdk/runtime/tools/data_analysis.py +183 -0
  135. alita_sdk/runtime/tools/function.py +100 -4
  136. alita_sdk/runtime/tools/graph.py +81 -0
  137. alita_sdk/runtime/tools/image_generation.py +218 -0
  138. alita_sdk/runtime/tools/llm.py +1013 -177
  139. alita_sdk/runtime/tools/loop.py +3 -1
  140. alita_sdk/runtime/tools/loop_output.py +3 -1
  141. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  142. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  143. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  144. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  145. alita_sdk/runtime/tools/planning/models.py +246 -0
  146. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  147. alita_sdk/runtime/tools/router.py +2 -1
  148. alita_sdk/runtime/tools/sandbox.py +375 -0
  149. alita_sdk/runtime/tools/skill_router.py +776 -0
  150. alita_sdk/runtime/tools/tool.py +3 -1
  151. alita_sdk/runtime/tools/vectorstore.py +69 -65
  152. alita_sdk/runtime/tools/vectorstore_base.py +163 -90
  153. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  154. alita_sdk/runtime/utils/mcp_client.py +492 -0
  155. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  156. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  157. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  158. alita_sdk/runtime/utils/streamlit.py +41 -14
  159. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  160. alita_sdk/runtime/utils/utils.py +48 -0
  161. alita_sdk/tools/__init__.py +135 -37
  162. alita_sdk/tools/ado/__init__.py +2 -2
  163. alita_sdk/tools/ado/repos/__init__.py +15 -19
  164. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  165. alita_sdk/tools/ado/test_plan/__init__.py +26 -8
  166. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  167. alita_sdk/tools/ado/wiki/__init__.py +27 -12
  168. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  169. alita_sdk/tools/ado/work_item/__init__.py +27 -12
  170. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  171. alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
  172. alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
  173. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  174. alita_sdk/tools/azure_ai/search/__init__.py +13 -8
  175. alita_sdk/tools/base/tool.py +5 -1
  176. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  177. alita_sdk/tools/bitbucket/__init__.py +27 -19
  178. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  179. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  180. alita_sdk/tools/browser/__init__.py +41 -16
  181. alita_sdk/tools/browser/crawler.py +3 -1
  182. alita_sdk/tools/browser/utils.py +15 -6
  183. alita_sdk/tools/carrier/__init__.py +18 -17
  184. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  185. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  186. alita_sdk/tools/chunkers/__init__.py +3 -1
  187. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  188. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  189. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  190. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  191. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  192. alita_sdk/tools/cloud/aws/__init__.py +11 -7
  193. alita_sdk/tools/cloud/azure/__init__.py +11 -7
  194. alita_sdk/tools/cloud/gcp/__init__.py +11 -7
  195. alita_sdk/tools/cloud/k8s/__init__.py +11 -7
  196. alita_sdk/tools/code/linter/__init__.py +9 -8
  197. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  198. alita_sdk/tools/code/sonar/__init__.py +20 -13
  199. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  200. alita_sdk/tools/confluence/__init__.py +21 -14
  201. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  202. alita_sdk/tools/confluence/loader.py +14 -2
  203. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  204. alita_sdk/tools/elastic/__init__.py +10 -8
  205. alita_sdk/tools/elitea_base.py +546 -64
  206. alita_sdk/tools/figma/__init__.py +11 -8
  207. alita_sdk/tools/figma/api_wrapper.py +352 -153
  208. alita_sdk/tools/github/__init__.py +17 -17
  209. alita_sdk/tools/github/api_wrapper.py +9 -26
  210. alita_sdk/tools/github/github_client.py +81 -12
  211. alita_sdk/tools/github/schemas.py +2 -1
  212. alita_sdk/tools/github/tool.py +5 -1
  213. alita_sdk/tools/gitlab/__init__.py +18 -13
  214. alita_sdk/tools/gitlab/api_wrapper.py +224 -80
  215. alita_sdk/tools/gitlab_org/__init__.py +13 -10
  216. alita_sdk/tools/google/bigquery/__init__.py +13 -13
  217. alita_sdk/tools/google/bigquery/tool.py +5 -1
  218. alita_sdk/tools/google_places/__init__.py +20 -11
  219. alita_sdk/tools/jira/__init__.py +21 -11
  220. alita_sdk/tools/jira/api_wrapper.py +315 -168
  221. alita_sdk/tools/keycloak/__init__.py +10 -8
  222. alita_sdk/tools/localgit/__init__.py +8 -3
  223. alita_sdk/tools/localgit/local_git.py +62 -54
  224. alita_sdk/tools/localgit/tool.py +5 -1
  225. alita_sdk/tools/memory/__init__.py +38 -14
  226. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  227. alita_sdk/tools/ocr/__init__.py +10 -8
  228. alita_sdk/tools/openapi/__init__.py +281 -108
  229. alita_sdk/tools/openapi/api_wrapper.py +883 -0
  230. alita_sdk/tools/openapi/tool.py +20 -0
  231. alita_sdk/tools/pandas/__init__.py +18 -11
  232. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  233. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  234. alita_sdk/tools/postman/__init__.py +10 -11
  235. alita_sdk/tools/postman/api_wrapper.py +19 -8
  236. alita_sdk/tools/postman/postman_analysis.py +8 -1
  237. alita_sdk/tools/pptx/__init__.py +10 -10
  238. alita_sdk/tools/qtest/__init__.py +21 -14
  239. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  240. alita_sdk/tools/rally/__init__.py +12 -10
  241. alita_sdk/tools/report_portal/__init__.py +22 -16
  242. alita_sdk/tools/salesforce/__init__.py +21 -16
  243. alita_sdk/tools/servicenow/__init__.py +20 -16
  244. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  245. alita_sdk/tools/sharepoint/__init__.py +16 -14
  246. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  247. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  248. alita_sdk/tools/sharepoint/utils.py +8 -2
  249. alita_sdk/tools/slack/__init__.py +11 -7
  250. alita_sdk/tools/sql/__init__.py +21 -19
  251. alita_sdk/tools/sql/api_wrapper.py +71 -23
  252. alita_sdk/tools/testio/__init__.py +20 -13
  253. alita_sdk/tools/testrail/__init__.py +12 -11
  254. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  255. alita_sdk/tools/utils/__init__.py +28 -4
  256. alita_sdk/tools/utils/content_parser.py +182 -62
  257. alita_sdk/tools/utils/text_operations.py +254 -0
  258. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  259. alita_sdk/tools/xray/__init__.py +17 -14
  260. alita_sdk/tools/xray/api_wrapper.py +58 -113
  261. alita_sdk/tools/yagmail/__init__.py +8 -3
  262. alita_sdk/tools/zephyr/__init__.py +11 -7
  263. alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
  264. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  265. alita_sdk/tools/zephyr_essential/__init__.py +15 -10
  266. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  267. alita_sdk/tools/zephyr_essential/client.py +6 -4
  268. alita_sdk/tools/zephyr_scale/__init__.py +12 -8
  269. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  270. alita_sdk/tools/zephyr_squad/__init__.py +11 -7
  271. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
  272. alita_sdk-0.3.562.dist-info/RECORD +450 -0
  273. alita_sdk-0.3.562.dist-info/entry_points.txt +2 -0
  274. alita_sdk/tools/bitbucket/tools.py +0 -304
  275. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  276. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/WHEEL +0 -0
  277. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
  278. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import re
2
3
  from typing import Union, Any, Optional, Annotated, get_type_hints
3
4
  from uuid import uuid4
4
5
  from typing import Dict
@@ -11,6 +12,7 @@ from langchain_core.runnables import Runnable
11
12
  from langchain_core.runnables import RunnableConfig
12
13
  from langchain_core.tools import BaseTool, ToolException
13
14
  from langgraph.channels.ephemeral_value import EphemeralValue
15
+ from langgraph.errors import GraphRecursionError
14
16
  from langgraph.graph import StateGraph
15
17
  from langgraph.graph.graph import END, START
16
18
  from langgraph.graph.state import CompiledStateGraph
@@ -18,8 +20,9 @@ from langgraph.managed.base import is_managed_value
18
20
  from langgraph.prebuilt import InjectedStore
19
21
  from langgraph.store.base import BaseStore
20
22
 
23
+ from .constants import PRINTER_NODE_RS, PRINTER, PRINTER_COMPLETED_STATE
21
24
  from .mixedAgentRenderes import convert_message_to_json
22
- from .utils import create_state, propagate_the_input_mapping
25
+ from .utils import create_state, propagate_the_input_mapping, safe_format
23
26
  from ..tools.function import FunctionTool
24
27
  from ..tools.indexer_tool import IndexerNode
25
28
  from ..tools.llm import LLMNode
@@ -27,7 +30,7 @@ from ..tools.loop import LoopNode
27
30
  from ..tools.loop_output import LoopToolNode
28
31
  from ..tools.tool import ToolNode
29
32
  from ..utils.evaluate import EvaluateTemplate
30
- from ..utils.utils import clean_string, TOOLKIT_SPLITTER
33
+ from ..utils.utils import clean_string
31
34
  from ..tools.router import RouterNode
32
35
 
33
36
  logger = logging.getLogger(__name__)
@@ -169,12 +172,13 @@ Answer only with step name, no need to add descrip in case none of the steps are
169
172
  """
170
173
 
171
174
  def __init__(self, client, steps: str, description: str = "", decisional_inputs: Optional[list[str]] = [],
172
- default_output: str = 'END'):
175
+ default_output: str = 'END', is_node: bool = False):
173
176
  self.client = client
174
177
  self.steps = ",".join([clean_string(step) for step in steps])
175
178
  self.description = description
176
179
  self.decisional_inputs = decisional_inputs
177
180
  self.default_output = default_output if default_output != 'END' else END
181
+ self.is_node = is_node
178
182
 
179
183
  def invoke(self, state: Annotated[BaseStore, InjectedStore()], config: Optional[RunnableConfig] = None) -> str:
180
184
  additional_info = ""
@@ -187,7 +191,7 @@ Answer only with step name, no need to add descrip in case none of the steps are
187
191
  additional_info = """### Additoinal info: """
188
192
  additional_info += "{field}: {value}\n".format(field=field, value=state.get(field, ""))
189
193
  decision_input.append(HumanMessage(
190
- self.prompt.format(steps=self.steps, description=self.description, additional_info=additional_info)))
194
+ self.prompt.format(steps=self.steps, description=safe_format(self.description, state), additional_info=additional_info)))
191
195
  completion = self.client.invoke(decision_input)
192
196
  result = clean_string(completion.content.strip())
193
197
  logger.info(f"Plan to transition to: {result}")
@@ -196,7 +200,8 @@ Answer only with step name, no need to add descrip in case none of the steps are
196
200
  dispatch_custom_event(
197
201
  "on_decision_edge", {"decisional_inputs": self.decisional_inputs, "state": state}, config=config
198
202
  )
199
- return result
203
+ # support of legacy `decision` as part of node
204
+ return {"router_output": result} if self.is_node else result
200
205
 
201
206
 
202
207
  class TransitionalEdge(Runnable):
@@ -231,6 +236,35 @@ class StateDefaultNode(Runnable):
231
236
  result[key] = temp_value
232
237
  return result
233
238
 
239
+ class PrinterNode(Runnable):
240
+ name = "PrinterNode"
241
+
242
+ def __init__(self, input_mapping: Optional[dict[str, dict]]):
243
+ self.input_mapping = input_mapping
244
+
245
+ def invoke(self, state: BaseStore, config: Optional[RunnableConfig] = None) -> dict:
246
+ logger.info(f"Printer Node - Current state variables: {state}")
247
+ result = {}
248
+ logger.debug(f"Initial text pattern: {self.input_mapping}")
249
+ mapping = propagate_the_input_mapping(self.input_mapping, [], state)
250
+ # for printer node we expect that all the lists will be joined into strings already
251
+ # Join any lists that haven't been converted yet
252
+ for key, value in mapping.items():
253
+ if isinstance(value, list):
254
+ mapping[key] = ', '.join(str(item) for item in value)
255
+ if mapping.get(PRINTER) is None:
256
+ raise ToolException(f"PrinterNode requires '{PRINTER}' field in input mapping")
257
+ formatted_output = mapping[PRINTER]
258
+ # add info label to the printer's output
259
+ if not formatted_output == PRINTER_COMPLETED_STATE:
260
+ # convert formatted output to string if it's not
261
+ if not isinstance(formatted_output, str):
262
+ formatted_output = str(formatted_output)
263
+ formatted_output += f"\n\n-----\n*How to proceed?*\n* *to resume the pipeline - type anything...*"
264
+ logger.debug(f"Formatted output: {formatted_output}")
265
+ result[PRINTER_NODE_RS] = formatted_output
266
+ return result
267
+
234
268
 
235
269
  class StateModifierNode(Runnable):
236
270
  name = "StateModifierNode"
@@ -248,19 +282,82 @@ class StateModifierNode(Runnable):
248
282
 
249
283
  # Collect input variables from state
250
284
  input_data = {}
285
+
251
286
  for var in self.input_variables:
252
287
  if var in state:
253
288
  input_data[var] = state.get(var)
254
-
289
+ type_of_output = type(state.get(self.output_variables[0])) if self.output_variables else None
255
290
  # Render the template using Jinja
256
- from jinja2 import Template
257
- rendered_message = Template(self.template).render(**input_data)
291
+ import json
292
+ import base64
293
+ from jinja2 import Environment
294
+
295
+ def from_json(value):
296
+ """Convert JSON string to Python object"""
297
+ try:
298
+ return json.loads(value)
299
+ except (json.JSONDecodeError, TypeError) as e:
300
+ logger.warning(f"Failed to parse JSON value: {e}")
301
+ return value
302
+
303
+ def base64_to_string(value):
304
+ """Convert base64 encoded string to regular string"""
305
+ try:
306
+ return base64.b64decode(value).decode('utf-8')
307
+ except Exception as e:
308
+ logger.warning(f"Failed to decode base64 value: {e}")
309
+ return value
310
+
311
+ def split_by_words(value, chunk_size=100):
312
+ words = value.split()
313
+ return [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
314
+
315
+ def split_by_regex(value, pattern):
316
+ """Splits the provided string using the specified regex pattern."""
317
+ return re.split(pattern, value)
318
+
319
+ env = Environment()
320
+ env.filters['from_json'] = from_json
321
+ env.filters['base64_to_string'] = base64_to_string
322
+ env.filters['split_by_words'] = split_by_words
323
+ env.filters['split_by_regex'] = split_by_regex
324
+
325
+ template = env.from_string(self.template)
326
+ rendered_message = template.render(**input_data)
258
327
  result = {}
259
328
  # Store the rendered message in the state or messages
260
329
  if len(self.output_variables) > 0:
261
330
  # Use the first output variable to store the rendered content
262
331
  output_var = self.output_variables[0]
263
- result[output_var] = rendered_message
332
+
333
+ # Convert rendered_message to the appropriate type
334
+ if type_of_output is not None:
335
+ try:
336
+ if type_of_output == dict:
337
+ result[output_var] = json.loads(rendered_message) if isinstance(rendered_message, str) else dict(rendered_message)
338
+ elif type_of_output == list:
339
+ result[output_var] = json.loads(rendered_message) if isinstance(rendered_message, str) else list(rendered_message)
340
+ elif type_of_output == int:
341
+ result[output_var] = int(rendered_message)
342
+ elif type_of_output == float:
343
+ result[output_var] = float(rendered_message)
344
+ elif type_of_output == str:
345
+ result[output_var] = str(rendered_message)
346
+ elif type_of_output == bool:
347
+ if isinstance(rendered_message, str):
348
+ result[output_var] = rendered_message.lower() in ('true', '1', 'yes', 'on')
349
+ else:
350
+ result[output_var] = bool(rendered_message)
351
+ elif type_of_output == type(None):
352
+ result[output_var] = None
353
+ else:
354
+ # Fallback to string if type is not recognized
355
+ result[output_var] = str(rendered_message)
356
+ except (ValueError, TypeError, json.JSONDecodeError) as e:
357
+ logger.warning(f"Failed to convert rendered_message to {type_of_output.__name__}: {e}. Using string fallback.")
358
+ result[output_var] = str(rendered_message)
359
+ else:
360
+ result[output_var] = rendered_message
264
361
 
265
362
  # Clean up specified variables (make them empty, not delete)
266
363
 
@@ -284,8 +381,8 @@ class StateModifierNode(Runnable):
284
381
  return result
285
382
 
286
383
 
287
-
288
- def prepare_output_schema(lg_builder, memory, store, debug=False, interrupt_before=None, interrupt_after=None, state_class=None, output_variables=None):
384
+ def prepare_output_schema(lg_builder, memory, store, debug=False, interrupt_before=None, interrupt_after=None,
385
+ state_class=None, output_variables=None):
289
386
  # prepare output channels
290
387
  if interrupt_after is None:
291
388
  interrupt_after = []
@@ -386,30 +483,31 @@ def create_graph(
386
483
  node_id = clean_string(node['id'])
387
484
  toolkit_name = node.get('toolkit_name')
388
485
  tool_name = clean_string(node.get('tool', node_id))
389
- if toolkit_name:
390
- tool_name = f"{clean_string(toolkit_name)}{TOOLKIT_SPLITTER}{tool_name}"
486
+ # Tool names are now clean (no prefix needed)
391
487
  logger.info(f"Node: {node_id} : {node_type} - {tool_name}")
392
- if node_type in ['function', 'tool', 'loop', 'loop_from_tool', 'indexer', 'subgraph', 'pipeline', 'agent']:
488
+ if node_type in ['function', 'toolkit', 'mcp', 'tool', 'loop', 'loop_from_tool', 'indexer', 'subgraph', 'pipeline', 'agent']:
489
+ if node_type == 'mcp' and tool_name not in [tool.name for tool in tools]:
490
+ # MCP is not connected and node cannot be added
491
+ raise ToolException(f"MCP tool '{tool_name}' not found in the provided tools. "
492
+ f"Make sure it is connected properly. Available tools: {[tool.name for tool in tools]}")
393
493
  for tool in tools:
394
494
  if tool.name == tool_name:
395
- if node_type == 'function':
495
+ if node_type in ['function', 'toolkit', 'mcp']:
396
496
  lg_builder.add_node(node_id, FunctionTool(
397
- tool=tool, name=node['id'], return_type='dict',
497
+ tool=tool, name=node_id, return_type='dict',
398
498
  output_variables=node.get('output', []),
399
499
  input_mapping=node.get('input_mapping',
400
500
  {'messages': {'type': 'variable', 'value': 'messages'}}),
401
501
  input_variables=node.get('input', ['messages'])))
402
502
  elif node_type == 'agent':
403
503
  input_params = node.get('input', ['messages'])
404
- input_mapping = {'task': {'type': 'fstring', 'value': f"{node.get('task', '')}"},
405
- 'chat_history': {'type': 'fixed', 'value': []}}
406
- # Add 'chat_history' to input_mapping only if 'messages' is in input_params
407
- if 'messages' in input_params:
408
- input_mapping['chat_history'] = {'type': 'variable', 'value': 'messages'}
504
+ input_mapping = node.get('input_mapping',
505
+ {'messages': {'type': 'variable', 'value': 'messages'}})
506
+ output_vars = node.get('output', [])
409
507
  lg_builder.add_node(node_id, FunctionTool(
410
508
  client=client, tool=tool,
411
- name=node['id'], return_type='dict',
412
- output_variables=node.get('output', []),
509
+ name=node_id, return_type='str',
510
+ output_variables=output_vars + ['messages'] if 'messages' not in output_vars else output_vars,
413
511
  input_variables=input_params,
414
512
  input_mapping= input_mapping
415
513
  ))
@@ -420,9 +518,10 @@ def create_graph(
420
518
  # wrap with mappings
421
519
  pipeline_name = node.get('tool', None)
422
520
  if not pipeline_name:
423
- raise ValueError("Subgraph must have a 'tool' node: add required tool to the subgraph node")
521
+ raise ValueError(
522
+ "Subgraph must have a 'tool' node: add required tool to the subgraph node")
424
523
  node_fn = SubgraphRunnable(
425
- inner=tool,
524
+ inner=tool.graph,
426
525
  name=pipeline_name,
427
526
  input_mapping=node.get('input_mapping', {}),
428
527
  output_mapping=node.get('output_mapping', {}),
@@ -432,25 +531,16 @@ def create_graph(
432
531
  elif node_type == 'tool':
433
532
  lg_builder.add_node(node_id, ToolNode(
434
533
  client=client, tool=tool,
435
- name=node['id'], return_type='dict',
534
+ name=node_id, return_type='dict',
436
535
  output_variables=node.get('output', []),
437
536
  input_variables=node.get('input', ['messages']),
438
537
  structured_output=node.get('structured_output', False),
439
538
  task=node.get('task')
440
539
  ))
441
- # TODO: decide on struct output for agent nodes
442
- # elif node_type == 'agent':
443
- # lg_builder.add_node(node_id, AgentNode(
444
- # client=client, tool=tool,
445
- # name=node['id'], return_type='dict',
446
- # output_variables=node.get('output', []),
447
- # input_variables=node.get('input', ['messages']),
448
- # task=node.get('task')
449
- # ))
450
540
  elif node_type == 'loop':
451
541
  lg_builder.add_node(node_id, LoopNode(
452
542
  client=client, tool=tool,
453
- name=node['id'], return_type='dict',
543
+ name=node_id, return_type='dict',
454
544
  output_variables=node.get('output', []),
455
545
  input_variables=node.get('input', ['messages']),
456
546
  task=node.get('task', '')
@@ -459,13 +549,14 @@ def create_graph(
459
549
  loop_toolkit_name = node.get('loop_toolkit_name')
460
550
  loop_tool_name = node.get('loop_tool')
461
551
  if (loop_toolkit_name and loop_tool_name) or loop_tool_name:
462
- loop_tool_name = f"{clean_string(loop_toolkit_name)}{TOOLKIT_SPLITTER}{loop_tool_name}" if loop_toolkit_name else clean_string(loop_tool_name)
552
+ # Use clean tool name (no prefix)
553
+ loop_tool_name = clean_string(loop_tool_name)
463
554
  for t in tools:
464
555
  if t.name == loop_tool_name:
465
556
  logger.debug(f"Loop tool discovered: {t}")
466
557
  lg_builder.add_node(node_id, LoopToolNode(
467
558
  client=client,
468
- name=node['id'], return_type='dict',
559
+ name=node_id, return_type='dict',
469
560
  tool=tool, loop_tool=t,
470
561
  variables_mapping=node.get('variables_mapping', {}),
471
562
  output_variables=node.get('output', []),
@@ -485,13 +576,26 @@ def create_graph(
485
576
  client=client, tool=tool,
486
577
  index_tool=indexer_tool,
487
578
  input_mapping=node.get('input_mapping', {}),
488
- name=node['id'], return_type='dict',
579
+ name=node_id, return_type='dict',
489
580
  chunking_tool=node.get('chunking_tool', None),
490
581
  chunking_config=node.get('chunking_config', {}),
491
582
  output_variables=node.get('output', []),
492
583
  input_variables=node.get('input', ['messages']),
493
584
  structured_output=node.get('structured_output', False)))
494
585
  break
586
+ elif node_type == 'code':
587
+ from ..tools.sandbox import create_sandbox_tool
588
+ sandbox_tool = create_sandbox_tool(stateful=False, allow_net=True,
589
+ alita_client=kwargs.get('alita_client', None))
590
+ code_data = node.get('code', {'type': 'fixed', 'value': "return 'Code block is empty'"})
591
+ lg_builder.add_node(node_id, FunctionTool(
592
+ tool=sandbox_tool, name=node['id'], return_type='dict',
593
+ output_variables=node.get('output', []),
594
+ input_mapping={'code': code_data},
595
+ input_variables=node.get('input', ['messages']),
596
+ structured_output=node.get('structured_output', False),
597
+ alita_client=kwargs.get('alita_client', None)
598
+ ))
495
599
  elif node_type == 'llm':
496
600
  output_vars = node.get('output', [])
497
601
  output_vars_dict = {
@@ -504,10 +608,10 @@ def create_graph(
504
608
  tool_names = []
505
609
  if isinstance(connected_tools, dict):
506
610
  for toolkit, selected_tools in connected_tools.items():
507
- for tool in selected_tools:
508
- tool_names.append(f"{toolkit}{TOOLKIT_SPLITTER}{tool}")
611
+ # Add tool names directly (no prefix)
612
+ tool_names.extend(selected_tools)
509
613
  elif isinstance(connected_tools, list):
510
- # for cases when tools are provided as a list of names with already bound toolkit_name
614
+ # Use provided tool names as-is
511
615
  tool_names = connected_tools
512
616
 
513
617
  if tool_names:
@@ -520,28 +624,41 @@ def create_graph(
520
624
  else:
521
625
  # Use all available tools
522
626
  available_tools = [tool for tool in tools if isinstance(tool, BaseTool)]
523
-
627
+
524
628
  lg_builder.add_node(node_id, LLMNode(
525
- client=client,
526
- prompt=node.get('prompt', {}),
527
- name=node['id'],
629
+ client=client,
630
+ input_mapping=node.get('input_mapping', {'messages': {'type': 'variable', 'value': 'messages'}}),
631
+ name=node_id,
528
632
  return_type='dict',
529
- response_key=node.get('response_key', 'messages'),
530
633
  structured_output_dict=output_vars_dict,
531
634
  output_variables=output_vars,
532
635
  input_variables=node.get('input', ['messages']),
533
636
  structured_output=node.get('structured_output', False),
637
+ tool_execution_timeout=node.get('tool_execution_timeout', 900),
534
638
  available_tools=available_tools,
535
- tool_names=tool_names))
536
- elif node_type == 'router':
537
- # Add a RouterNode as an independent node
538
- lg_builder.add_node(node_id, RouterNode(
539
- name=node['id'],
540
- condition=node.get('condition', ''),
541
- routes=node.get('routes', []),
542
- default_output=node.get('default_output', 'END'),
543
- input_variables=node.get('input', ['messages'])
639
+ tool_names=tool_names,
640
+ steps_limit=kwargs.get('steps_limit', 25)
544
641
  ))
642
+ elif node_type in ['router', 'decision']:
643
+ if node_type == 'router':
644
+ # Add a RouterNode as an independent node
645
+ lg_builder.add_node(node_id, RouterNode(
646
+ name=node_id,
647
+ condition=node.get('condition', ''),
648
+ routes=node.get('routes', []),
649
+ default_output=node.get('default_output', 'END'),
650
+ input_variables=node.get('input', ['messages'])
651
+ ))
652
+ elif node_type == 'decision':
653
+ logger.info(f'Adding decision: {node["nodes"]}')
654
+ lg_builder.add_node(node_id, DecisionEdge(
655
+ client, node['nodes'],
656
+ node.get('description', ""),
657
+ decisional_inputs=node.get('decisional_inputs', ['messages']),
658
+ default_output=node.get('default_output', 'END'),
659
+ is_node=True
660
+ ))
661
+
545
662
  # Add a single conditional edge for all routes
546
663
  lg_builder.add_conditional_edges(
547
664
  node_id,
@@ -552,6 +669,7 @@ def create_graph(
552
669
  default_output=node.get('default_output', 'END')
553
670
  )
554
671
  )
672
+ continue
555
673
  elif node_type == 'state_modifier':
556
674
  lg_builder.add_node(node_id, StateModifierNode(
557
675
  template=node.get('template', ''),
@@ -559,6 +677,22 @@ def create_graph(
559
677
  input_variables=node.get('input', ['messages']),
560
678
  output_variables=node.get('output', [])
561
679
  ))
680
+ elif node_type == 'printer':
681
+ lg_builder.add_node(node_id, PrinterNode(
682
+ input_mapping=node.get('input_mapping', {'printer': {'type': 'fixed', 'value': ''}}),
683
+ ))
684
+
685
+ # add interrupts after printer node if specified
686
+ interrupt_after.append(clean_string(node_id))
687
+
688
+ # reset printer output variable to avoid carrying over
689
+ reset_node_id = f"{node_id}_reset"
690
+ lg_builder.add_node(reset_node_id, PrinterNode(
691
+ input_mapping={'printer': {'type': 'fixed', 'value': PRINTER_COMPLETED_STATE}}
692
+ ))
693
+ lg_builder.add_conditional_edges(node_id, TransitionalEdge(reset_node_id))
694
+ lg_builder.add_conditional_edges(reset_node_id, TransitionalEdge(clean_string(node['transition'])))
695
+ continue
562
696
  if node.get('transition'):
563
697
  next_step = clean_string(node['transition'])
564
698
  logger.info(f'Adding transition: {next_step}')
@@ -584,14 +718,11 @@ def create_graph(
584
718
  entry_point = clean_string(schema['entry_point'])
585
719
  except KeyError:
586
720
  raise ToolException("Entry point is not defined in the schema. Please define 'entry_point' in the schema.")
587
- for key, value in state.items():
588
- if 'type' in value and 'value' in value:
589
- # set default value for state variable if it is defined in the schema
590
- state_default_node = StateDefaultNode(default_vars=state)
591
- lg_builder.add_node(state_default_node.name, state_default_node)
592
- lg_builder.set_entry_point(state_default_node.name)
593
- lg_builder.add_conditional_edges(state_default_node.name, TransitionalEdge(entry_point))
594
- break
721
+ if state.items():
722
+ state_default_node = StateDefaultNode(default_vars=set_defaults(state))
723
+ lg_builder.add_node(state_default_node.name, state_default_node)
724
+ lg_builder.set_entry_point(state_default_node.name)
725
+ lg_builder.add_conditional_edges(state_default_node.name, TransitionalEdge(entry_point))
595
726
  else:
596
727
  # if no state variables are defined, set the entry point directly
597
728
  lg_builder.set_entry_point(entry_point)
@@ -633,6 +764,38 @@ def create_graph(
633
764
  )
634
765
  return compiled.validate()
635
766
 
767
+ def set_defaults(d):
768
+ """Set default values for dictionary entries based on their type."""
769
+ type_defaults = {
770
+ 'str': '',
771
+ 'list': [],
772
+ 'dict': {},
773
+ 'int': 0,
774
+ 'float': 0.0,
775
+ 'bool': False,
776
+ # add more types as needed
777
+ }
778
+ # Build state_types mapping with STRING type names (not actual type objects)
779
+ state_types = {}
780
+
781
+ for k, v in d.items():
782
+ # Skip 'input' key as it is not a state initial variable
783
+ if k == 'input':
784
+ continue
785
+ # set value or default if type is defined
786
+ if 'value' not in v:
787
+ v['value'] = type_defaults.get(v['type'], None)
788
+
789
+ # Also build the state_types mapping with STRING type names
790
+ var_type = v['type'] if isinstance(v, dict) else v
791
+ if var_type in ['str', 'int', 'float', 'bool', 'list', 'dict', 'number']:
792
+ # Store the string type name, not the actual type object
793
+ state_types[k] = var_type if var_type != 'number' else 'int'
794
+
795
+ # Add state_types as a default value that will be set at initialization
796
+ # Use string type names to avoid serialization issues
797
+ d['state_types'] = {'type': 'dict', 'value': state_types}
798
+ return d
636
799
 
637
800
  def convert_dict_to_message(msg_dict):
638
801
  """Convert a dictionary message to a LangChain message object."""
@@ -665,56 +828,208 @@ class LangGraphAgentRunnable(CompiledStateGraph):
665
828
  def invoke(self, input: Union[dict[str, Any], Any],
666
829
  config: Optional[RunnableConfig] = None,
667
830
  *args, **kwargs):
668
- logger.info(f"Incomming Input: {input}")
669
- if not config.get("configurable", {}).get("thread_id"):
831
+ logger.info(f"Incoming Input: {input}")
832
+ if config is None:
833
+ config = RunnableConfig()
834
+ if not config.get("configurable", {}).get("thread_id", ""):
670
835
  config["configurable"] = {"thread_id": str(uuid4())}
671
836
  thread_id = config.get("configurable", {}).get("thread_id")
837
+
838
+ # Check if checkpoint exists early for chat_history handling
839
+ checkpoint_exists = self.checkpointer and self.checkpointer.get_tuple(config)
840
+
672
841
  # Handle chat history and current input properly
673
842
  if input.get('chat_history') and not input.get('messages'):
674
- # Convert chat history dict messages to LangChain message objects
675
- chat_history = input.pop('chat_history')
676
- input['messages'] = [convert_dict_to_message(msg) for msg in chat_history]
677
-
843
+ if checkpoint_exists:
844
+ # Checkpoint already has conversation history - discard redundant chat_history
845
+ input.pop('chat_history', None)
846
+ else:
847
+ # No checkpoint - convert chat history dict messages to LangChain message objects
848
+ chat_history = input.pop('chat_history')
849
+ input['messages'] = [convert_dict_to_message(msg) for msg in chat_history]
850
+
851
+ # handler for LLM node: if no input (Chat perspective), then take last human message
852
+ # Track if input came from messages to handle content extraction properly
853
+ input_from_messages = False
854
+ if not input.get('input'):
855
+ if input.get('messages'):
856
+ input['input'] = [next((msg for msg in reversed(input['messages']) if isinstance(msg, HumanMessage)),
857
+ None)]
858
+ if input['input'] is not None:
859
+ input_from_messages = True
860
+
678
861
  # Append current input to existing messages instead of overwriting
679
862
  if input.get('input'):
680
- current_message = HumanMessage(content=input.get('input'))
863
+ if isinstance(input['input'], str):
864
+ current_message = input['input']
865
+ else:
866
+ # input can be a list of messages or a single message object
867
+ current_message = input.get('input')[-1]
868
+
869
+ # TODO: add handler after we add 2+ inputs (filterByType, etc.)
870
+ if isinstance(current_message, HumanMessage):
871
+ current_content = current_message.content
872
+ if isinstance(current_content, list):
873
+ # Extract text parts and keep non-text parts (images, etc.)
874
+ text_contents = []
875
+ non_text_parts = []
876
+
877
+ for item in current_content:
878
+ if isinstance(item, dict) and item.get('type') == 'text':
879
+ text_contents.append(item['text'])
880
+ elif isinstance(item, str):
881
+ text_contents.append(item)
882
+ else:
883
+ # Keep image_url and other non-text content
884
+ non_text_parts.append(item)
885
+
886
+ # Set input to the joined text
887
+ input['input'] = ". ".join(text_contents) if text_contents else ""
888
+
889
+ # If this message came from input['messages'], update or remove it
890
+ if input_from_messages:
891
+ if non_text_parts:
892
+ # Keep the message but only with non-text content (images, etc.)
893
+ current_message.content = non_text_parts
894
+ else:
895
+ # All content was text, remove this message from the list
896
+ input['messages'] = [msg for msg in input['messages'] if msg is not current_message]
897
+ else:
898
+ # Message came from input['input'], not from input['messages']
899
+ # If there are non-text parts (images, etc.), preserve them in messages
900
+ if non_text_parts:
901
+ # Initialize messages if it doesn't exist or is empty
902
+ if not input.get('messages'):
903
+ input['messages'] = []
904
+ # Create a new message with only non-text content
905
+ non_text_message = HumanMessage(content=non_text_parts)
906
+ input['messages'].append(non_text_message)
907
+
908
+ elif isinstance(current_content, str):
909
+ # on regenerate case
910
+ input['input'] = current_content
911
+ # If from messages and all content is text, remove the message
912
+ if input_from_messages:
913
+ input['messages'] = [msg for msg in input['messages'] if msg is not current_message]
914
+ else:
915
+ input['input'] = str(current_content)
916
+ # If from messages, remove since we extracted the content
917
+ if input_from_messages:
918
+ input['messages'] = [msg for msg in input['messages'] if msg is not current_message]
919
+ elif isinstance(current_message, str):
920
+ input['input'] = current_message
921
+ else:
922
+ input['input'] = str(current_message)
681
923
  if input.get('messages'):
682
924
  # Ensure existing messages are LangChain objects
683
925
  input['messages'] = [convert_dict_to_message(msg) for msg in input['messages']]
684
926
  # Append to existing messages
685
- input['messages'].append(current_message)
927
+ # input['messages'].append(current_message)
928
+ # else:
929
+ # NOTE: Commented out to prevent duplicates with input['input']
930
+ # input['messages'] = [current_message]
931
+
932
+ # Validate that input is not empty after all processing
933
+ if not input.get('input'):
934
+ raise RuntimeError(
935
+ "Empty input after processing. Cannot send empty string to LLM. "
936
+ "This likely means the message contained only non-text content "
937
+ "with no accompanying text."
938
+ )
939
+
940
+ logger.info(f"Input: {thread_id} - {input}")
941
+ try:
942
+ if self.checkpointer and self.checkpointer.get_tuple(config):
943
+ if config.pop("should_continue", False):
944
+ invoke_input = input
945
+ else:
946
+ self.update_state(config, input)
947
+ invoke_input = None
948
+ result = super().invoke(invoke_input, config=config, *args, **kwargs)
686
949
  else:
687
- # No existing messages, create new list
688
- input['messages'] = [current_message]
689
- logging.info(f"Input: {thread_id} - {input}")
690
- if self.checkpointer and self.checkpointer.get_tuple(config):
691
- self.update_state(config, input)
692
- result = super().invoke(None, config=config, *args, **kwargs)
693
- else:
694
- result = super().invoke(input, config=config, *args, **kwargs)
950
+ result = super().invoke(input, config=config, *args, **kwargs)
951
+ except GraphRecursionError as e:
952
+ current_recursion_limit = config.get("recursion_limit", 0)
953
+ logger.warning("ToolExecutionLimitReached caught in LangGraphAgentRunnable: %s", e)
954
+ return self._handle_graph_recursion_error(
955
+ config=config,
956
+ thread_id=thread_id,
957
+ current_recursion_limit=current_recursion_limit,
958
+ )
959
+
695
960
  try:
696
- if self.output_variables and self.output_variables[0] != "messages":
697
- # If output_variables are specified, use the value of first one or use the last messages as default
698
- output = result.get(self.output_variables[0], result['messages'][-1].content)
961
+ # Check if printer node output exists
962
+ printer_output = result.get(PRINTER_NODE_RS)
963
+ if printer_output == PRINTER_COMPLETED_STATE:
964
+ # Printer completed, extract last AI message
965
+ messages = result['messages']
966
+ output = next(
967
+ (msg.content for msg in reversed(messages)
968
+ if not isinstance(msg, HumanMessage)),
969
+ messages[-1].content
970
+ ) if messages else result.get('output')
971
+ elif printer_output is not None:
972
+ # Printer node has output (interrupted state)
973
+ output = printer_output
699
974
  else:
700
- output = result['messages'][-1].content
701
- except:
702
- output = list(result.values())[-1]
703
- thread_id = None
975
+ # No printer node, extract last AI message from messages
976
+ messages = result.get('messages', [])
977
+ output = next(
978
+ (msg.content for msg in reversed(messages)
979
+ if not isinstance(msg, HumanMessage)),
980
+ None
981
+ )
982
+ except Exception:
983
+ # Fallback: try to get last value or last message
984
+ output = str(list(result.values())[-1]) if result else 'Output is undefined'
704
985
  config_state = self.get_state(config)
705
- if config_state.next:
706
- thread_id = config['configurable']['thread_id']
986
+ is_execution_finished = not config_state.next
987
+ if is_execution_finished:
988
+ thread_id = None
989
+
990
+ final_output = f"Assistant run has been completed, but output is None.\nAdding last message if any: {messages[-1] if messages else []}" if is_execution_finished and output is None else output
707
991
 
708
992
  result_with_state = {
709
- "output": output,
993
+ "output": final_output,
710
994
  "thread_id": thread_id,
711
- "execution_finished": not config_state.next
995
+ "execution_finished": is_execution_finished
712
996
  }
713
997
 
714
998
  # Include all state values in the result
715
999
  if hasattr(config_state, 'values') and config_state.values:
1000
+ # except of key = 'output' which is already included
1001
+ for key, value in config_state.values.items():
1002
+ if key != 'output':
1003
+ result_with_state[key] = value
1004
+
1005
+ return result_with_state
1006
+
1007
+ def _handle_graph_recursion_error(
1008
+ self,
1009
+ config: RunnableConfig,
1010
+ thread_id: str,
1011
+ current_recursion_limit: int,
1012
+ ) -> dict:
1013
+ """Handle GraphRecursionError by returning a soft-boundary response."""
1014
+ config_state = self.get_state(config)
1015
+ is_execution_finished = False
1016
+
1017
+ friendly_output = (
1018
+ f"Tool step limit {current_recursion_limit} reached for this run. You can continue by sending another "
1019
+ "message or refining your request."
1020
+ )
1021
+
1022
+ result_with_state: dict[str, Any] = {
1023
+ "output": friendly_output,
1024
+ "thread_id": thread_id,
1025
+ "execution_finished": is_execution_finished,
1026
+ "tool_execution_limit_reached": True,
1027
+ }
1028
+
1029
+ if hasattr(config_state, "values") and config_state.values:
716
1030
  for key, value in config_state.values.items():
717
- result_with_state[key] = value
1031
+ if key != "output":
1032
+ result_with_state[key] = value
718
1033
 
719
1034
  return result_with_state
720
1035