Skip to content

Commit eae05bf

Browse files
authored
Merge pull request #408 from zhongkaifu/codex/add-data-node-type-with-schema-support
Add planner tools, model, executor and UI support for data nodes
2 parents 5153384 + 94d89df commit eae05bf

12 files changed

Lines changed: 800 additions & 244 deletions

File tree

tools/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"""Business-ready tools and registry exports."""
55
from tools.base import Tool
66
from tools.builtin import (
7-
ask_ai,
87
compose_outlook_email,
98
list_files,
109
read_file,
@@ -32,7 +31,6 @@
3231
"call_registered_tool",
3332
"register_builtin_tools",
3433
"search_web",
35-
"ask_ai",
3634
"list_files",
3735
"read_file",
3836
"summarize",

tools/builtin.py

Lines changed: 1 addition & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
register_sales_tools,
3232
register_web_scraper_tools,
3333
)
34-
from tools.registry import get_registered_tool, register_tool
34+
from tools.registry import register_tool
3535

3636

3737
def _normalize_web_url(raw_url: str) -> str:
@@ -199,199 +199,6 @@ def _clean_text(value: str) -> str:
199199
return {"results": results}
200200

201201

202-
def _prepare_ask_ai_prompt(
203-
*,
204-
prompt: str | None,
205-
question: str | None,
206-
context: Dict[str, Any] | None,
207-
expected_format: str | Mapping[str, Any] | None,
208-
) -> str:
209-
if prompt:
210-
return prompt
211-
212-
parts = ["You are a helpful assistant that produces concise JSON answers."]
213-
parts.append(f"Question: {question}")
214-
if context:
215-
context_json = json.dumps(context, ensure_ascii=False, indent=2)
216-
parts.append("Context (for reference):")
217-
parts.append(context_json)
218-
if expected_format:
219-
expected_format_text = (
220-
expected_format
221-
if isinstance(expected_format, str)
222-
else json.dumps(expected_format, ensure_ascii=False, indent=2)
223-
)
224-
parts.append("Return JSON following this guidance:")
225-
parts.append(expected_format_text)
226-
parts.append("Respond with valid JSON only.")
227-
return "\n\n".join(parts)
228-
229-
230-
def _build_call_tool_schema(action: Mapping[str, Any]) -> Optional[Dict[str, Any]]:
231-
action_id = action.get("action_id") or action.get("tool_name") or "tool"
232-
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", action.get("tool_name") or action_id)
233-
description = action.get("description") or action.get("name") or action_id
234-
parameters = action.get("arg_schema") or action.get("params_schema")
235-
if not isinstance(parameters, Mapping):
236-
return None
237-
238-
return {
239-
"type": "function",
240-
"function": {
241-
"name": safe_name,
242-
"description": description,
243-
"parameters": parameters,
244-
},
245-
}
246-
247-
248-
def _execute_business_tool(
249-
*, action: Mapping[str, Any], args: Mapping[str, Any], call_name: str
250-
) -> Dict[str, Any]:
251-
tool_fn_name = action.get("tool_name") or action.get("action_id") or call_name
252-
registered_tool = get_registered_tool(tool_fn_name)
253-
254-
if not registered_tool:
255-
return {
256-
"status": "unavailable",
257-
"action_id": action.get("action_id"),
258-
"message": f"Business tool '{tool_fn_name}' is not registered; returning the original arguments for reference.",
259-
"echo": args,
260-
}
261-
262-
try:
263-
output = registered_tool(**args)
264-
return {
265-
"status": "ok",
266-
"action_id": action.get("action_id"),
267-
"output": output,
268-
}
269-
except Exception as exc: # pragma: no cover - runtime errors are surfaced to LLM
270-
return {
271-
"status": "error",
272-
"action_id": action.get("action_id"),
273-
"message": str(exc),
274-
}
275-
276-
277-
def ask_ai(
278-
*,
279-
system_prompt: str | None = None,
280-
prompt: str | None = None,
281-
question: str | None = None,
282-
context: Dict[str, Any] | None = None,
283-
expected_format: str | Mapping[str, Any] | None = None,
284-
tool: List[Mapping[str, Any]] | None = None,
285-
model: str | None = None,
286-
max_tokens: int = 256,
287-
max_rounds: int = 3,
288-
) -> Dict[str, Any]:
289-
"""LLM helper that can optionally call business tools and analyze results."""
290-
291-
if not (prompt or question):
292-
raise ValueError("either prompt or question must be provided for ask_ai")
293-
294-
user_prompt = _prepare_ask_ai_prompt(
295-
prompt=prompt, question=question, context=context, expected_format=expected_format
296-
)
297-
system_text = system_prompt or "You are an intelligent assistant skilled at invoking business tools to solve problems."
298-
299-
client = OpenAI()
300-
chat_model = model or OPENAI_MODEL
301-
messages: List[Dict[str, Any]] = [
302-
{"role": "system", "content": system_text},
303-
{"role": "user", "content": user_prompt},
304-
]
305-
306-
tools_spec: List[Dict[str, Any]] = []
307-
tool_lookup: Dict[str, Mapping[str, Any]] = {}
308-
for item in tool or []:
309-
schema = _build_call_tool_schema(item)
310-
if schema:
311-
tool_name = schema["function"]["name"]
312-
tools_spec.append(schema)
313-
tool_lookup[tool_name] = item
314-
315-
for round_idx in range(1, max_rounds + 1):
316-
response = client.chat.completions.create(
317-
model=chat_model,
318-
messages=messages,
319-
tools=tools_spec or None,
320-
tool_choice="auto" if tools_spec else None,
321-
max_completion_tokens=max_tokens,
322-
)
323-
if not response.choices:
324-
raise RuntimeError("ask_ai did not return any choices")
325-
326-
message = response.choices[0].message
327-
assistant_msg = {
328-
"role": "assistant",
329-
"content": message.content or "",
330-
"tool_calls": message.tool_calls,
331-
}
332-
messages.append(assistant_msg)
333-
log_info(f"[ask_ai] round={round_idx} assistant response received")
334-
if assistant_msg.get("content"):
335-
log_json("[ask_ai] assistant content", assistant_msg.get("content"))
336-
else:
337-
reminder = (
338-
"Your previous response had no content. "
339-
"Please provide a concise response that satisfies the user's requirements."
340-
)
341-
messages.append({"role": "user", "content": reminder})
342-
log_info("[ask_ai] assistant content empty; prompting for a complete response")
343-
344-
tool_calls = getattr(message, "tool_calls", None) or []
345-
if tool_calls:
346-
for tc in tool_calls:
347-
func_name = tc.function.name
348-
raw_args = tc.function.arguments
349-
try:
350-
parsed_args = json.loads(raw_args) if raw_args else {}
351-
except json.JSONDecodeError:
352-
parsed_args = {"__raw__": raw_args or ""}
353-
354-
action_def = tool_lookup.get(func_name)
355-
if not action_def:
356-
tool_result = {
357-
"status": "error",
358-
"message": f"Unknown business tool invocation: {func_name}",
359-
"echo": parsed_args,
360-
}
361-
else:
362-
tool_result = _execute_business_tool(
363-
action=action_def, args=parsed_args, call_name=func_name
364-
)
365-
366-
messages.append(
367-
{
368-
"role": "tool",
369-
"tool_call_id": tc.id,
370-
"content": json.dumps(tool_result, ensure_ascii=False),
371-
}
372-
)
373-
log_info(f"[ask_ai] round={round_idx} tool_call={func_name}")
374-
log_json("[ask_ai] tool_args", parsed_args)
375-
log_json("[ask_ai] tool_result", tool_result)
376-
continue
377-
378-
final_content = (message.content or "").strip()
379-
if not final_content:
380-
continue
381-
382-
try:
383-
parsed_content = json.loads(final_content)
384-
results = parsed_content if isinstance(parsed_content, dict) else {"answer": parsed_content}
385-
except json.JSONDecodeError:
386-
results = {"answer": final_content}
387-
388-
return {"status": "ok", "results": results, "messages": messages}
389-
390-
return {
391-
"status": "error",
392-
"results": {"message": "ask_ai could not produce an answer within the allowed rounds"},
393-
"messages": messages,
394-
}
395202

396203

397204
def classify_text(
@@ -767,31 +574,6 @@ def register_builtin_tools() -> None:
767574
)
768575
)
769576

770-
register_tool(
771-
Tool(
772-
name="ask_ai",
773-
description=(
774-
"LLM helper that can run multi-round chat with optional business tool calls "
775-
"and return normalized status/results output."
776-
),
777-
function=ask_ai,
778-
args_schema={
779-
"type": "object",
780-
"properties": {
781-
"system_prompt": {"type": "string", "nullable": True},
782-
"prompt": {"type": "string", "nullable": True},
783-
"question": {"type": "string", "nullable": True},
784-
"context": {"type": "object", "nullable": True},
785-
"expected_format": {"type": "string", "nullable": True},
786-
"tool": {"type": "array", "items": {"type": "object"}, "nullable": True},
787-
"model": {"type": "string", "nullable": True},
788-
"max_tokens": {"type": "integer", "default": 256},
789-
"max_rounds": {"type": "integer", "default": 3},
790-
},
791-
},
792-
)
793-
)
794-
795577
register_tool(
796578
Tool(
797579
name="classify_text",
@@ -1050,7 +832,6 @@ def register_builtin_tools() -> None:
1050832
"register_builtin_tools",
1051833
"search_web",
1052834
"search_news",
1053-
"ask_ai",
1054835
"classify_text",
1055836
"list_files",
1056837
"read_file",

velvetflow/bindings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,12 @@ def _validate_result_reference(self, src_path: str) -> None:
699699

700700
# 控制节点(condition 等)也允许被引用,缺少 action_id 时跳过 schema 校验
701701
if node.type != "action" or not node.action_id:
702+
output_schema = node.out_params_schema
703+
field_path = parts[2:]
704+
if output_schema and field_path and self._schema_has_path(output_schema, field_path):
705+
return
706+
if output_schema and not field_path:
707+
return
702708
return
703709

704710
action_id = node.action_id

0 commit comments

Comments
 (0)