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.
- 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 +3601 -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 +111 -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 +407 -92
- alita_sdk/runtime/langchain/utils.py +102 -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 +24 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +780 -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 +1013 -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/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 +15 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +26 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +27 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +27 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +13 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +27 -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 +11 -7
- alita_sdk/tools/cloud/azure/__init__.py +11 -7
- alita_sdk/tools/cloud/gcp/__init__.py +11 -7
- alita_sdk/tools/cloud/k8s/__init__.py +11 -7
- alita_sdk/tools/code/linter/__init__.py +9 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +20 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +21 -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 +11 -5
- alita_sdk/tools/elastic/__init__.py +10 -8
- alita_sdk/tools/elitea_base.py +546 -64
- alita_sdk/tools/figma/__init__.py +11 -8
- alita_sdk/tools/figma/api_wrapper.py +352 -153
- alita_sdk/tools/github/__init__.py +17 -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 +18 -13
- alita_sdk/tools/gitlab/api_wrapper.py +224 -80
- alita_sdk/tools/gitlab_org/__init__.py +13 -10
- alita_sdk/tools/google/bigquery/__init__.py +13 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +20 -11
- alita_sdk/tools/jira/__init__.py +21 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- alita_sdk/tools/keycloak/__init__.py +10 -8
- alita_sdk/tools/localgit/__init__.py +8 -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 +10 -8
- alita_sdk/tools/openapi/__init__.py +281 -108
- alita_sdk/tools/openapi/api_wrapper.py +883 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +18 -11
- 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 +10 -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 +10 -10
- alita_sdk/tools/qtest/__init__.py +21 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +12 -10
- alita_sdk/tools/report_portal/__init__.py +22 -16
- alita_sdk/tools/salesforce/__init__.py +21 -16
- alita_sdk/tools/servicenow/__init__.py +20 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +16 -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 +11 -7
- alita_sdk/tools/sql/__init__.py +21 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +20 -13
- alita_sdk/tools/testrail/__init__.py +12 -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 +182 -62
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +17 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +8 -3
- alita_sdk/tools/zephyr/__init__.py +11 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +15 -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 +12 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +11 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
- alita_sdk-0.3.562.dist-info/RECORD +450 -0
- alita_sdk-0.3.562.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.562.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
495
|
+
if node_type in ['function', 'toolkit', 'mcp']:
|
|
396
496
|
lg_builder.add_node(node_id, FunctionTool(
|
|
397
|
-
tool=tool, name=
|
|
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 =
|
|
405
|
-
|
|
406
|
-
|
|
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=
|
|
412
|
-
output_variables=
|
|
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(
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
508
|
-
|
|
611
|
+
# Add tool names directly (no prefix)
|
|
612
|
+
tool_names.extend(selected_tools)
|
|
509
613
|
elif isinstance(connected_tools, list):
|
|
510
|
-
#
|
|
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
|
-
|
|
527
|
-
name=
|
|
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
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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"
|
|
669
|
-
if
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
self.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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":
|
|
993
|
+
"output": final_output,
|
|
710
994
|
"thread_id": thread_id,
|
|
711
|
-
"execution_finished":
|
|
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
|
-
|
|
1031
|
+
if key != "output":
|
|
1032
|
+
result_with_state[key] = value
|
|
718
1033
|
|
|
719
1034
|
return result_with_state
|
|
720
1035
|
|