alita-sdk 0.3.532__py3-none-any.whl → 0.3.602__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (137) hide show
  1. alita_sdk/cli/agent_executor.py +2 -1
  2. alita_sdk/cli/agent_loader.py +34 -4
  3. alita_sdk/cli/agents.py +433 -203
  4. alita_sdk/community/__init__.py +8 -4
  5. alita_sdk/configurations/__init__.py +1 -0
  6. alita_sdk/configurations/openapi.py +323 -0
  7. alita_sdk/runtime/clients/client.py +165 -7
  8. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  9. alita_sdk/runtime/langchain/assistant.py +61 -11
  10. alita_sdk/runtime/langchain/constants.py +419 -171
  11. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -2
  12. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  13. alita_sdk/runtime/langchain/langraph_agent.py +108 -23
  14. alita_sdk/runtime/langchain/utils.py +76 -14
  15. alita_sdk/runtime/skills/__init__.py +91 -0
  16. alita_sdk/runtime/skills/callbacks.py +498 -0
  17. alita_sdk/runtime/skills/discovery.py +540 -0
  18. alita_sdk/runtime/skills/executor.py +610 -0
  19. alita_sdk/runtime/skills/input_builder.py +371 -0
  20. alita_sdk/runtime/skills/models.py +330 -0
  21. alita_sdk/runtime/skills/registry.py +355 -0
  22. alita_sdk/runtime/skills/skill_runner.py +330 -0
  23. alita_sdk/runtime/toolkits/__init__.py +5 -0
  24. alita_sdk/runtime/toolkits/artifact.py +2 -1
  25. alita_sdk/runtime/toolkits/mcp.py +6 -3
  26. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  27. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  28. alita_sdk/runtime/toolkits/tools.py +139 -10
  29. alita_sdk/runtime/toolkits/vectorstore.py +1 -1
  30. alita_sdk/runtime/tools/__init__.py +3 -1
  31. alita_sdk/runtime/tools/artifact.py +15 -0
  32. alita_sdk/runtime/tools/data_analysis.py +183 -0
  33. alita_sdk/runtime/tools/llm.py +260 -73
  34. alita_sdk/runtime/tools/loop.py +3 -1
  35. alita_sdk/runtime/tools/loop_output.py +3 -1
  36. alita_sdk/runtime/tools/mcp_server_tool.py +6 -3
  37. alita_sdk/runtime/tools/router.py +2 -4
  38. alita_sdk/runtime/tools/sandbox.py +9 -6
  39. alita_sdk/runtime/tools/skill_router.py +776 -0
  40. alita_sdk/runtime/tools/tool.py +3 -1
  41. alita_sdk/runtime/tools/vectorstore.py +7 -2
  42. alita_sdk/runtime/tools/vectorstore_base.py +7 -2
  43. alita_sdk/runtime/utils/constants.py +5 -1
  44. alita_sdk/runtime/utils/mcp_client.py +1 -1
  45. alita_sdk/runtime/utils/mcp_sse_client.py +1 -1
  46. alita_sdk/runtime/utils/toolkit_utils.py +2 -0
  47. alita_sdk/tools/__init__.py +44 -2
  48. alita_sdk/tools/ado/repos/__init__.py +26 -8
  49. alita_sdk/tools/ado/repos/repos_wrapper.py +78 -52
  50. alita_sdk/tools/ado/test_plan/__init__.py +3 -2
  51. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  52. alita_sdk/tools/ado/utils.py +1 -18
  53. alita_sdk/tools/ado/wiki/__init__.py +2 -1
  54. alita_sdk/tools/ado/wiki/ado_wrapper.py +23 -1
  55. alita_sdk/tools/ado/work_item/__init__.py +3 -2
  56. alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
  57. alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
  58. alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
  59. alita_sdk/tools/azure_ai/search/__init__.py +2 -1
  60. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  61. alita_sdk/tools/base_indexer_toolkit.py +51 -30
  62. alita_sdk/tools/bitbucket/__init__.py +2 -1
  63. alita_sdk/tools/bitbucket/api_wrapper.py +1 -1
  64. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +3 -3
  65. alita_sdk/tools/browser/__init__.py +1 -1
  66. alita_sdk/tools/carrier/__init__.py +1 -1
  67. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  68. alita_sdk/tools/cloud/aws/__init__.py +2 -1
  69. alita_sdk/tools/cloud/azure/__init__.py +2 -1
  70. alita_sdk/tools/cloud/gcp/__init__.py +2 -1
  71. alita_sdk/tools/cloud/k8s/__init__.py +2 -1
  72. alita_sdk/tools/code/linter/__init__.py +2 -1
  73. alita_sdk/tools/code/sonar/__init__.py +2 -1
  74. alita_sdk/tools/code_indexer_toolkit.py +19 -2
  75. alita_sdk/tools/confluence/__init__.py +7 -6
  76. alita_sdk/tools/confluence/api_wrapper.py +7 -8
  77. alita_sdk/tools/confluence/loader.py +4 -2
  78. alita_sdk/tools/custom_open_api/__init__.py +2 -1
  79. alita_sdk/tools/elastic/__init__.py +2 -1
  80. alita_sdk/tools/elitea_base.py +28 -9
  81. alita_sdk/tools/figma/__init__.py +52 -6
  82. alita_sdk/tools/figma/api_wrapper.py +1158 -123
  83. alita_sdk/tools/figma/figma_client.py +73 -0
  84. alita_sdk/tools/figma/toon_tools.py +2748 -0
  85. alita_sdk/tools/github/__init__.py +2 -1
  86. alita_sdk/tools/github/github_client.py +56 -92
  87. alita_sdk/tools/github/schemas.py +4 -4
  88. alita_sdk/tools/gitlab/__init__.py +2 -1
  89. alita_sdk/tools/gitlab/api_wrapper.py +118 -38
  90. alita_sdk/tools/gitlab_org/__init__.py +2 -1
  91. alita_sdk/tools/gitlab_org/api_wrapper.py +60 -62
  92. alita_sdk/tools/google/bigquery/__init__.py +2 -1
  93. alita_sdk/tools/google_places/__init__.py +2 -1
  94. alita_sdk/tools/jira/__init__.py +2 -1
  95. alita_sdk/tools/keycloak/__init__.py +2 -1
  96. alita_sdk/tools/localgit/__init__.py +2 -1
  97. alita_sdk/tools/memory/__init__.py +1 -1
  98. alita_sdk/tools/ocr/__init__.py +2 -1
  99. alita_sdk/tools/openapi/__init__.py +490 -118
  100. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  101. alita_sdk/tools/openapi/tool.py +20 -0
  102. alita_sdk/tools/pandas/__init__.py +11 -5
  103. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  104. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  105. alita_sdk/tools/postman/__init__.py +2 -1
  106. alita_sdk/tools/pptx/__init__.py +2 -1
  107. alita_sdk/tools/qtest/__init__.py +21 -2
  108. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  109. alita_sdk/tools/rally/__init__.py +2 -1
  110. alita_sdk/tools/rally/api_wrapper.py +1 -1
  111. alita_sdk/tools/report_portal/__init__.py +2 -1
  112. alita_sdk/tools/salesforce/__init__.py +2 -1
  113. alita_sdk/tools/servicenow/__init__.py +11 -10
  114. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  115. alita_sdk/tools/sharepoint/__init__.py +2 -1
  116. alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
  117. alita_sdk/tools/slack/__init__.py +3 -2
  118. alita_sdk/tools/slack/api_wrapper.py +2 -2
  119. alita_sdk/tools/sql/__init__.py +3 -2
  120. alita_sdk/tools/testio/__init__.py +2 -1
  121. alita_sdk/tools/testrail/__init__.py +2 -1
  122. alita_sdk/tools/utils/content_parser.py +77 -3
  123. alita_sdk/tools/utils/text_operations.py +163 -71
  124. alita_sdk/tools/xray/__init__.py +3 -2
  125. alita_sdk/tools/yagmail/__init__.py +2 -1
  126. alita_sdk/tools/zephyr/__init__.py +2 -1
  127. alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
  128. alita_sdk/tools/zephyr_essential/__init__.py +2 -1
  129. alita_sdk/tools/zephyr_scale/__init__.py +3 -2
  130. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  131. alita_sdk/tools/zephyr_squad/__init__.py +2 -1
  132. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/METADATA +7 -6
  133. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/RECORD +137 -119
  134. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/WHEEL +0 -0
  135. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/entry_points.txt +0 -0
  136. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/licenses/LICENSE +0 -0
  137. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
1
+ """
2
+ Data Analysis internal tool for Alita SDK.
3
+
4
+ This tool provides Pandas-based data analysis capabilities as an internal tool,
5
+ accessible through the "Enable internal tools" dropdown menu.
6
+
7
+ It uses the conversation attachment bucket for file storage, providing seamless
8
+ integration with drag-and-drop file uploads in chat.
9
+ """
10
+ import logging
11
+ from typing import Any, List, Literal, Optional, Type
12
+
13
+ from langchain_core.tools import BaseTool, BaseToolkit
14
+ from pydantic import BaseModel, ConfigDict, create_model, Field
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ name = "data_analysis"
19
+
20
+
21
+ def get_tools(tools_list: list, alita_client=None, llm=None, memory_store=None):
22
+ """
23
+ Get data analysis tools for the provided tool configurations.
24
+
25
+ Args:
26
+ tools_list: List of tool configurations
27
+ alita_client: Alita client instance (required for data analysis)
28
+ llm: LLM client instance (required for code generation)
29
+ memory_store: Optional memory store instance (unused)
30
+
31
+ Returns:
32
+ List of data analysis tools
33
+ """
34
+ all_tools = []
35
+
36
+ for tool in tools_list:
37
+ if (tool.get('type') == 'data_analysis' or
38
+ tool.get('toolkit_name') == 'data_analysis'):
39
+ try:
40
+ if not alita_client:
41
+ logger.error("Alita client is required for data analysis tools")
42
+ continue
43
+
44
+ settings = tool.get('settings', {})
45
+ bucket_name = settings.get('bucket_name')
46
+
47
+ if not bucket_name:
48
+ logger.error("bucket_name is required for data analysis tools")
49
+ continue
50
+
51
+ toolkit_instance = DataAnalysisToolkit.get_toolkit(
52
+ alita_client=alita_client,
53
+ llm=llm,
54
+ bucket_name=bucket_name,
55
+ toolkit_name=tool.get('toolkit_name', '')
56
+ )
57
+ all_tools.extend(toolkit_instance.get_tools())
58
+ except Exception as e:
59
+ logger.error(f"Error in data analysis toolkit get_tools: {e}")
60
+ logger.error(f"Tool config: {tool}")
61
+ raise
62
+
63
+ return all_tools
64
+
65
+
66
+ class DataAnalysisToolkit(BaseToolkit):
67
+ """
68
+ Data Analysis toolkit providing Pandas-based data analysis capabilities.
69
+
70
+ This is an internal tool that uses the conversation attachment bucket
71
+ for file storage, enabling seamless integration with chat file uploads.
72
+ """
73
+ tools: List[BaseTool] = []
74
+
75
+ @staticmethod
76
+ def toolkit_config_schema() -> Type[BaseModel]:
77
+ """Get the configuration schema for the data analysis toolkit."""
78
+ # Import PandasWrapper to get available tools schema
79
+ from alita_sdk.tools.pandas.api_wrapper import PandasWrapper
80
+
81
+ selected_tools = {
82
+ x['name']: x['args_schema'].model_json_schema()
83
+ for x in PandasWrapper.model_construct().get_available_tools()
84
+ }
85
+
86
+ return create_model(
87
+ 'data_analysis',
88
+ bucket_name=(
89
+ Optional[str],
90
+ Field(
91
+ default=None,
92
+ title="Bucket name",
93
+ description="Bucket where files are stored (auto-injected from conversation)"
94
+ )
95
+ ),
96
+ selected_tools=(
97
+ List[Literal[tuple(selected_tools)]],
98
+ Field(
99
+ default=[],
100
+ json_schema_extra={'args_schemas': selected_tools}
101
+ )
102
+ ),
103
+ __config__=ConfigDict(json_schema_extra={
104
+ 'metadata': {
105
+ "label": "Data Analysis",
106
+ "icon_url": "data-analysis.svg",
107
+ "hidden": True, # Hidden from regular toolkit menu
108
+ "categories": ["internal_tool"],
109
+ "extra_categories": ["data analysis", "pandas", "dataframes", "data science"],
110
+ }
111
+ })
112
+ )
113
+
114
+ @classmethod
115
+ def get_toolkit(
116
+ cls,
117
+ alita_client=None,
118
+ llm=None,
119
+ bucket_name: str = None,
120
+ toolkit_name: Optional[str] = None,
121
+ selected_tools: Optional[List[str]] = None,
122
+ **kwargs
123
+ ):
124
+ """
125
+ Get toolkit with data analysis tools.
126
+
127
+ Args:
128
+ alita_client: Alita client instance (required)
129
+ llm: LLM for code generation (optional, uses alita_client.llm if not provided)
130
+ bucket_name: Conversation attachment bucket (required)
131
+ toolkit_name: Optional name prefix for tools
132
+ selected_tools: Optional list of tool names to include (default: all)
133
+ **kwargs: Additional arguments
134
+
135
+ Returns:
136
+ DataAnalysisToolkit instance with configured tools
137
+
138
+ Raises:
139
+ ValueError: If alita_client or bucket_name is not provided
140
+ """
141
+ if not alita_client:
142
+ raise ValueError("Alita client is required for data analysis")
143
+
144
+ if not bucket_name:
145
+ raise ValueError("bucket_name is required for data analysis (should be conversation attachment bucket)")
146
+
147
+ # Import the PandasWrapper from existing toolkit
148
+ from alita_sdk.tools.pandas.api_wrapper import PandasWrapper
149
+ from alita_sdk.tools.base.tool import BaseAction
150
+
151
+ # Create wrapper with conversation bucket
152
+ wrapper = PandasWrapper(
153
+ alita=alita_client,
154
+ llm=llm,
155
+ bucket_name=bucket_name
156
+ )
157
+
158
+ # Get tools from wrapper
159
+ available_tools = wrapper.get_available_tools()
160
+ tools = []
161
+
162
+ for tool in available_tools:
163
+ # Filter by selected_tools if provided
164
+ if selected_tools and tool["name"] not in selected_tools:
165
+ continue
166
+
167
+ description = tool["description"]
168
+ if toolkit_name:
169
+ description = f"Toolkit: {toolkit_name}\n{description}"
170
+ description = description[:1000]
171
+
172
+ tools.append(BaseAction(
173
+ api_wrapper=wrapper,
174
+ name=tool["name"],
175
+ description=description,
176
+ args_schema=tool["args_schema"],
177
+ metadata={"toolkit_name": toolkit_name, "toolkit_type": name} if toolkit_name else {}
178
+ ))
179
+
180
+ return cls(tools=tools)
181
+
182
+ def get_tools(self):
183
+ return self.tools
@@ -6,7 +6,6 @@ from typing import Any, Optional, List, Union, Literal
6
6
  from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
7
7
  from langchain_core.runnables import RunnableConfig
8
8
  from langchain_core.tools import BaseTool, ToolException
9
- from langchain_core.exceptions import OutputParserException
10
9
  from langchain_core.callbacks import dispatch_custom_event
11
10
  from pydantic import Field
12
11
 
@@ -44,6 +43,17 @@ logger = logging.getLogger(__name__)
44
43
 
45
44
  # return supports_reasoning
46
45
 
46
+ JSON_INSTRUCTION_TEMPLATE = (
47
+ "\n\n**IMPORTANT: You MUST respond with ONLY a valid JSON object.**\n\n"
48
+ "Required JSON fields:\n{field_descriptions}\n\n"
49
+ "Example format:\n"
50
+ "{{\n{example_fields}\n}}\n\n"
51
+ "Rules:\n"
52
+ "1. Output ONLY the JSON object - no markdown, no explanations, no extra text\n"
53
+ "2. Ensure all required fields are present\n"
54
+ "3. Use proper JSON syntax with double quotes for strings\n"
55
+ "4. Do not wrap the JSON in code blocks or backticks"
56
+ )
47
57
 
48
58
  class LLMNode(BaseTool):
49
59
  """Enhanced LLM node with chat history and tool binding support"""
@@ -67,6 +77,240 @@ class LLMNode(BaseTool):
67
77
  steps_limit: Optional[int] = Field(default=25, description='Maximum steps for tool execution')
68
78
  tool_execution_timeout: Optional[int] = Field(default=900, description='Timeout (seconds) for tool execution. Default is 15 minutes.')
69
79
 
80
+ def _prepare_structured_output_params(self) -> dict:
81
+ """
82
+ Prepare structured output parameters from structured_output_dict.
83
+
84
+ Expected self.structured_output_dict formats:
85
+ - {"field": "str"} / {"field": "list"} / {"field": "list[str]"} / {"field": "any"} ...
86
+ - OR {"field": {"type": "...", "description": "...", "default": ...}} (optional)
87
+
88
+ Returns:
89
+ Dict[str, Dict] suitable for create_pydantic_model(...)
90
+ """
91
+ struct_params: dict[str, dict] = {}
92
+
93
+ for key, value in (self.structured_output_dict or {}).items():
94
+ # Allow either a plain type string or a dict with details
95
+ if isinstance(value, dict):
96
+ type_str = (value.get("type") or "any")
97
+ desc = value.get("description", "") or ""
98
+ entry: dict = {"type": type_str, "description": desc}
99
+ if "default" in value:
100
+ entry["default"] = value["default"]
101
+ else:
102
+ type_str = (value or "any") if isinstance(value, str) else "any"
103
+ entry = {"type": type_str, "description": ""}
104
+
105
+ # Normalize: only convert the *exact* "list" into "list[str]"
106
+ # (avoid the old bug where "if 'list' in value" also hits "blacklist", etc.)
107
+ if isinstance(entry.get("type"), str) and entry["type"].strip().lower() == "list":
108
+ entry["type"] = "list[str]"
109
+
110
+ struct_params[key] = entry
111
+
112
+ # Add default output field for proper response to user
113
+ struct_params[ELITEA_RS] = {
114
+ "description": "final output to user (summarized output from LLM)",
115
+ "type": "str",
116
+ "default": None,
117
+ }
118
+
119
+ return struct_params
120
+
121
+ def _invoke_with_structured_output(self, llm_client: Any, messages: List, struct_model: Any, config: RunnableConfig):
122
+ """
123
+ Invoke LLM with structured output, handling tool calls if present.
124
+
125
+ Args:
126
+ llm_client: LLM client instance
127
+ messages: List of conversation messages
128
+ struct_model: Pydantic model for structured output
129
+ config: Runnable configuration
130
+
131
+ Returns:
132
+ Tuple of (completion, initial_completion, final_messages)
133
+ """
134
+ initial_completion = llm_client.invoke(messages, config=config)
135
+
136
+ if hasattr(initial_completion, 'tool_calls') and initial_completion.tool_calls:
137
+ # Handle tool calls first, then apply structured output
138
+ new_messages, _ = self._run_async_in_sync_context(
139
+ self.__perform_tool_calling(initial_completion, messages, llm_client, config)
140
+ )
141
+ llm = self.__get_struct_output_model(llm_client, struct_model)
142
+ completion = llm.invoke(new_messages, config=config)
143
+ return completion, initial_completion, new_messages
144
+ else:
145
+ # Direct structured output without tool calls
146
+ llm = self.__get_struct_output_model(llm_client, struct_model)
147
+ completion = llm.invoke(messages, config=config)
148
+ return completion, initial_completion, messages
149
+
150
+ def _build_json_instruction(self, struct_model: Any) -> str:
151
+ """
152
+ Build JSON instruction message for fallback handling.
153
+
154
+ Args:
155
+ struct_model: Pydantic model with field definitions
156
+
157
+ Returns:
158
+ Formatted JSON instruction string
159
+ """
160
+ field_descriptions = []
161
+ for name, field in struct_model.model_fields.items():
162
+ field_type = field.annotation.__name__ if hasattr(field.annotation, '__name__') else str(field.annotation)
163
+ field_desc = field.description or field_type
164
+ field_descriptions.append(f" - {name} ({field_type}): {field_desc}")
165
+
166
+ example_fields = ",\n".join([
167
+ f' "{k}": <{field.annotation.__name__ if hasattr(field.annotation, "__name__") else "value"}>'
168
+ for k, field in struct_model.model_fields.items()
169
+ ])
170
+
171
+ return JSON_INSTRUCTION_TEMPLATE.format(
172
+ field_descriptions="\n".join(field_descriptions),
173
+ example_fields=example_fields
174
+ )
175
+
176
+ def _create_fallback_completion(self, content: str, struct_model: Any) -> Any:
177
+ """
178
+ Create a fallback completion object when JSON parsing fails.
179
+
180
+ Args:
181
+ content: Plain text content from LLM
182
+ struct_model: Pydantic model to construct
183
+
184
+ Returns:
185
+ Pydantic model instance with fallback values
186
+ """
187
+ result_dict = {}
188
+ for k, field in struct_model.model_fields.items():
189
+ if k == ELITEA_RS:
190
+ result_dict[k] = content
191
+ elif field.is_required():
192
+ # Set default values for required fields based on type
193
+ result_dict[k] = field.default if field.default is not None else None
194
+ else:
195
+ result_dict[k] = field.default
196
+ return struct_model.model_construct(**result_dict)
197
+
198
+ def _handle_structured_output_fallback(self, llm_client: Any, messages: List, struct_model: Any,
199
+ config: RunnableConfig, original_error: Exception) -> Any:
200
+ """
201
+ Handle structured output fallback through multiple strategies.
202
+
203
+ Tries fallback methods in order:
204
+ 1. json_mode with explicit instructions
205
+ 2. function_calling method
206
+ 3. Plain text with JSON extraction
207
+
208
+ Args:
209
+ llm_client: LLM client instance
210
+ messages: Original conversation messages
211
+ struct_model: Pydantic model for structured output
212
+ config: Runnable configuration
213
+ original_error: The original ValueError that triggered fallback
214
+
215
+ Returns:
216
+ Completion with structured output (best effort)
217
+
218
+ Raises:
219
+ Propagates exceptions from LLM invocation
220
+ """
221
+ logger.error(f"Error invoking structured output model: {format_exc()}")
222
+ logger.info("Attempting to fall back to json mode")
223
+
224
+ # Build JSON instruction once
225
+ json_instruction = self._build_json_instruction(struct_model)
226
+
227
+ # Add instruction to messages
228
+ modified_messages = messages.copy()
229
+ if modified_messages and isinstance(modified_messages[-1], HumanMessage):
230
+ modified_messages[-1] = HumanMessage(
231
+ content=modified_messages[-1].content + json_instruction
232
+ )
233
+ else:
234
+ modified_messages.append(HumanMessage(content=json_instruction))
235
+
236
+ # Try json_mode with explicit instructions
237
+ try:
238
+ completion = self.__get_struct_output_model(
239
+ llm_client, struct_model, method="json_mode"
240
+ ).invoke(modified_messages, config=config)
241
+ return completion
242
+ except Exception as json_mode_error:
243
+ logger.warning(f"json_mode also failed: {json_mode_error}")
244
+ logger.info("Falling back to function_calling method")
245
+
246
+ # Try function_calling as a third fallback
247
+ try:
248
+ completion = self.__get_struct_output_model(
249
+ llm_client, struct_model, method="function_calling"
250
+ ).invoke(modified_messages, config=config)
251
+ return completion
252
+ except Exception as function_calling_error:
253
+ logger.error(f"function_calling also failed: {function_calling_error}")
254
+ logger.info("Final fallback: using plain LLM response")
255
+
256
+ # Last resort: get plain text response and wrap in structure
257
+ plain_completion = llm_client.invoke(modified_messages, config=config)
258
+ content = plain_completion.content.strip() if hasattr(plain_completion, 'content') else str(plain_completion)
259
+
260
+ # Try to extract JSON from the response
261
+ import json
262
+ import re
263
+
264
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
265
+ if json_match:
266
+ try:
267
+ parsed_json = json.loads(json_match.group(0))
268
+ # Validate it has expected fields and wrap in pydantic model
269
+ completion = struct_model(**parsed_json)
270
+ return completion
271
+ except (json.JSONDecodeError, Exception) as parse_error:
272
+ logger.warning(f"Could not parse extracted JSON: {parse_error}")
273
+ return self._create_fallback_completion(content, struct_model)
274
+ else:
275
+ # No JSON found, create response with content in elitea_response
276
+ return self._create_fallback_completion(content, struct_model)
277
+
278
+ def _format_structured_output_result(self, result: dict, messages: List, initial_completion: Any) -> dict:
279
+ """
280
+ Format structured output result with properly formatted messages.
281
+
282
+ Args:
283
+ result: Result dictionary from model_dump()
284
+ messages: Original conversation messages
285
+ initial_completion: Initial completion before tool calls
286
+
287
+ Returns:
288
+ Formatted result dictionary with messages
289
+ """
290
+ # Ensure messages are properly formatted
291
+ if result.get('messages') and isinstance(result['messages'], list):
292
+ result['messages'] = [{'role': 'assistant', 'content': '\n'.join(result['messages'])}]
293
+ else:
294
+ # Extract content from initial_completion, handling thinking blocks
295
+ fallback_content = result.get(ELITEA_RS, '')
296
+ if not fallback_content and initial_completion:
297
+ content_parts = self._extract_content_from_completion(initial_completion)
298
+ fallback_content = content_parts.get('text') or ''
299
+ thinking = content_parts.get('thinking')
300
+
301
+ # Log thinking if present
302
+ if thinking:
303
+ logger.debug(f"Thinking content present in structured output: {thinking[:100]}...")
304
+
305
+ if not fallback_content:
306
+ # Final fallback to raw content
307
+ content = initial_completion.content
308
+ fallback_content = content if isinstance(content, str) else str(content)
309
+
310
+ result['messages'] = messages + [AIMessage(content=fallback_content)]
311
+
312
+ return result
313
+
70
314
  def get_filtered_tools(self) -> List[BaseTool]:
71
315
  """
72
316
  Filter available tools based on tool_names list.
@@ -164,8 +408,6 @@ class LLMNode(BaseTool):
164
408
  if func_args.get('system') is None or func_args.get('task') is None:
165
409
  raise ToolException(f"LLMNode requires 'system' and 'task' parameters in input mapping. "
166
410
  f"Actual params: {func_args}")
167
- raise ToolException(f"LLMNode requires 'system' and 'task' parameters in input mapping. "
168
- f"Actual params: {func_args}")
169
411
  # cast to str in case user passes variable different from str
170
412
  messages = [SystemMessage(content=str(func_args.get('system'))), *func_args.get('chat_history', []), HumanMessage(content=str(func_args.get('task')))]
171
413
  # Remove pre-last item if last two messages are same type and content
@@ -197,78 +439,23 @@ class LLMNode(BaseTool):
197
439
  try:
198
440
  if self.structured_output and self.output_variables:
199
441
  # Handle structured output
200
- struct_params = {
201
- key: {
202
- "type": 'list[str]' if 'list' in value else value,
203
- "description": ""
204
- }
205
- for key, value in (self.structured_output_dict or {}).items()
206
- }
207
- # Add default output field for proper response to user
208
- struct_params['elitea_response'] = {
209
- 'description': 'final output to user (summarized output from LLM)', 'type': 'str',
210
- "default": None}
442
+ struct_params = self._prepare_structured_output_params()
211
443
  struct_model = create_pydantic_model(f"LLMOutput", struct_params)
212
- initial_completion = llm_client.invoke(messages, config=config)
213
- if hasattr(initial_completion, 'tool_calls') and initial_completion.tool_calls:
214
- new_messages, _ = self._run_async_in_sync_context(
215
- self.__perform_tool_calling(initial_completion, messages, llm_client, config)
444
+
445
+ try:
446
+ completion, initial_completion, final_messages = self._invoke_with_structured_output(
447
+ llm_client, messages, struct_model, config
216
448
  )
217
- llm = self.__get_struct_output_model(llm_client, struct_model)
218
- completion = llm.invoke(new_messages, config=config)
219
- result = completion.model_dump()
220
- else:
221
- try:
222
- llm = self.__get_struct_output_model(llm_client, struct_model)
223
- completion = llm.invoke(messages, config=config)
224
- except (ValueError, OutputParserException) as e:
225
- logger.error(f"Error invoking structured output model: {format_exc()}")
226
- logger.info("Attempting to fall back to json mode")
227
- try:
228
- # Fallback to regular LLM with JSON extraction
229
- completion = self.__get_struct_output_model(llm_client, struct_model,
230
- method="json_mode").invoke(messages, config=config)
231
- except (ValueError, OutputParserException) as e2:
232
- logger.error(f"json_mode fallback also failed: {format_exc()}")
233
- logger.info("Attempting to fall back to function_calling")
234
- # Final fallback to function_calling method
235
- completion = self.__get_struct_output_model(llm_client, struct_model,
236
- method="json_schema").invoke(messages, config=config)
237
- result = completion.model_dump()
238
-
239
- # Ensure messages are properly formatted
240
- if result.get('messages') and isinstance(result['messages'], list):
241
- result['messages'] = [{'role': 'assistant', 'content': '\n'.join(result['messages'])}]
242
- else:
243
- # Extract content from initial_completion, handling thinking blocks
244
- fallback_content = result.get(ELITEA_RS, '')
245
- if not fallback_content and initial_completion:
246
- content_parts = self._extract_content_from_completion(initial_completion)
247
- fallback_content = content_parts.get('text') or ''
248
- thinking = content_parts.get('thinking')
249
-
250
- # Dispatch thinking event if present
251
- if thinking:
252
- try:
253
- model_name = getattr(llm_client, 'model_name', None) or getattr(llm_client, 'model', 'LLM')
254
- dispatch_custom_event(
255
- name="thinking_step",
256
- data={
257
- "message": thinking,
258
- "tool_name": f"LLM ({model_name})",
259
- "toolkit": "reasoning",
260
- },
261
- config=config,
262
- )
263
- except Exception as e:
264
- logger.warning(f"Failed to dispatch thinking event: {e}")
265
-
266
- if not fallback_content:
267
- # Final fallback to raw content
268
- content = initial_completion.content
269
- fallback_content = content if isinstance(content, str) else str(content)
270
-
271
- result['messages'] = messages + [AIMessage(content=fallback_content)]
449
+ except ValueError as e:
450
+ # Handle fallback for structured output failures
451
+ completion = self._handle_structured_output_fallback(
452
+ llm_client, messages, struct_model, config, e
453
+ )
454
+ initial_completion = None
455
+ final_messages = messages
456
+
457
+ result = completion.model_dump()
458
+ result = self._format_structured_output_result(result, final_messages, initial_completion or completion)
272
459
 
273
460
  return result
274
461
  else:
@@ -102,7 +102,9 @@ Input Data:
102
102
  logger.debug(f"LoopNode input: {predict_input}")
103
103
  completion = self.client.invoke(predict_input, config=config)
104
104
  logger.debug(f"LoopNode pure output: {completion}")
105
- loop_data = _old_extract_json(completion.content.strip())
105
+ from ..langchain.utils import extract_text_from_completion
106
+ content_text = extract_text_from_completion(completion)
107
+ loop_data = _old_extract_json(content_text.strip())
106
108
  logger.debug(f"LoopNode output: {loop_data}")
107
109
  if self.return_type == "str":
108
110
  accumulated_response = ''
@@ -93,7 +93,9 @@ Answer must be JSON only extractable by JSON.LOADS."""
93
93
  else:
94
94
  input_[-1].content += self.unstructured_output
95
95
  completion = self.client.invoke(input_, config=config)
96
- result = _extract_json(completion.content.strip())
96
+ from ..langchain.utils import extract_text_from_completion
97
+ content_text = extract_text_from_completion(completion)
98
+ result = _extract_json(content_text.strip())
97
99
  try:
98
100
  tool_result: dict | List[dict] = self.tool.invoke(result, config=config, kwargs=kwargs)
99
101
  dispatch_custom_event(
@@ -1,9 +1,12 @@
1
1
  import uuid
2
2
  from logging import getLogger
3
- from typing import Any, Type, Literal, Optional, Union, List
3
+ from typing import Any, Type, Literal, Optional, Union, List, Annotated
4
4
 
5
5
  from langchain_core.tools import BaseTool
6
- from pydantic import BaseModel, Field, create_model, EmailStr, constr, ConfigDict
6
+ from pydantic import BaseModel, Field, create_model, ConfigDict, StringConstraints
7
+
8
+ # EmailStr moved to pydantic_extra_types in pydantic v2, use str for simplicity
9
+ EmailStr = str
7
10
 
8
11
  logger = getLogger(__name__)
9
12
 
@@ -59,7 +62,7 @@ class McpServerTool(BaseTool):
59
62
  if field.get("format") == "email":
60
63
  return EmailStr
61
64
  if "pattern" in field:
62
- return constr(regex=field["pattern"])
65
+ return Annotated[str, StringConstraints(pattern=field["pattern"])]
63
66
  return str
64
67
  if t == "integer":
65
68
  return int
@@ -27,10 +27,8 @@ class RouterNode(BaseTool):
27
27
  if result in [clean_string(formatted_result) for formatted_result in self.routes]:
28
28
  # If the result is one of the routes, return it
29
29
  return {"router_output": result}
30
- elif result == self.default_output:
31
- # If the result is the default output, return it
32
- return {"router_output": clean_string(self.default_output)}
33
- return {"router_output": 'END'}
30
+ # For any unmatched condition (including empty string), use the configured default_output
31
+ return {"router_output": clean_string(self.default_output)}
34
32
 
35
33
  def _run(self, *args, **kwargs):
36
34
  return self.invoke(**kwargs)
@@ -326,12 +326,15 @@ class SandboxToolkit(BaseToolkit):
326
326
 
327
327
  @staticmethod
328
328
  def toolkit_config_schema() -> Type[BaseModel]:
329
- # Create sample tools to get their schemas
330
- sample_tools = [
331
- PyodideSandboxTool(),
332
- StatefulPyodideSandboxTool()
333
- ]
334
- selected_tools = {x.name: x.args_schema.model_json_schema() for x in sample_tools}
329
+ # Get tool schemas without instantiating the tools (avoids Deno requirement)
330
+ try:
331
+ selected_tools = {
332
+ "pyodide_sandbox": sandbox_tool_input.model_json_schema(),
333
+ "stateful_pyodide_sandbox": sandbox_tool_input.model_json_schema(),
334
+ }
335
+ except Exception as e:
336
+ logger.warning(f"Could not generate sandbox tool schemas: {e}")
337
+ selected_tools = {}
335
338
 
336
339
  return create_model(
337
340
  'sandbox',