forked from cleveragents/cleveragents-core
Feat: Added finish command to scientific paper writer
This commit is contained in:
@@ -36,11 +36,11 @@ run_step_with_capture() {
|
||||
|
||||
run_step python -m cleveragents context delete --all --yes
|
||||
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer_langgraph.yaml --context "bookmark" -p "Hello"
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer.yaml --context "bookmark" -p "Hello"
|
||||
|
||||
while IFS= read -r prompt; do
|
||||
[ -z "${prompt}" ] && continue
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer_langgraph.yaml --context "bookmark" -p "${prompt}"
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer.yaml --context "bookmark" -p "${prompt}"
|
||||
done <<'EOF'
|
||||
!next
|
||||
COVID19
|
||||
@@ -62,14 +62,14 @@ EOF
|
||||
while :; do
|
||||
for prompt in "Suggest five additional sources specific to this section to the list" "!write" "!proofread" "!accept"; do
|
||||
if [ "${prompt}" = "!accept" ]; then
|
||||
run_step_with_capture python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer_langgraph.yaml --context "bookmark" -p "${prompt}"
|
||||
run_step_with_capture python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer.yaml --context "bookmark" -p "${prompt}"
|
||||
case "${RUN_STEP_CAPTURE_OUTPUT}" in
|
||||
*!next*)
|
||||
break 2
|
||||
;;
|
||||
esac
|
||||
else
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer_langgraph.yaml --context "bookmark" -p "${prompt}"
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer.yaml --context "bookmark" -p "${prompt}"
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -77,4 +77,4 @@ done
|
||||
run_step rm -rf at_bookmark.json
|
||||
|
||||
run_step python -m cleveragents context export bookmark at_bookmark.json
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer_langgraph.yaml --load-context at_bookmark.json -p '!next'
|
||||
run_step python -m cleveragents run -t 0 --unsafe --config examples/scientific_paper_writer.yaml --load-context at_bookmark.json -p '!next'
|
||||
|
||||
+1606
-576
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,419 +0,0 @@
|
||||
# Scientific Paper Writer - Message Router Version
|
||||
# Uses the generic message_router node type for user-defined routing patterns
|
||||
|
||||
name: scientific_paper_writer_with_router
|
||||
|
||||
# Context configuration
|
||||
context:
|
||||
global:
|
||||
stage_order:
|
||||
- intro
|
||||
- discovery
|
||||
- brainstorming
|
||||
- vetting
|
||||
- structure
|
||||
- section_writing
|
||||
- paper_review
|
||||
- latex_generation
|
||||
writing_stage: null
|
||||
paper_details:
|
||||
topic: null
|
||||
length: null
|
||||
audience: null
|
||||
publication: null
|
||||
format: null
|
||||
other: null
|
||||
|
||||
# Agent definitions
|
||||
agents:
|
||||
workflow_controller:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.7
|
||||
system_prompt: |
|
||||
You are the workflow controller for a scientific paper writing assistant.
|
||||
Guide users through the paper writing process step by step.
|
||||
|
||||
When responding to commands:
|
||||
- For "!next": Emit "GOTO_DISCOVERY:" followed by a message
|
||||
- For "!stage": Show the current stage
|
||||
- For "!help": Show available commands
|
||||
|
||||
Available routing commands you can emit:
|
||||
- GOTO_INTRO: Start introduction
|
||||
- GOTO_DISCOVERY: Start discovery phase
|
||||
- GOTO_BRAINSTORMING: Start brainstorming
|
||||
- GOTO_VETTING: Start vetting
|
||||
- GOTO_STRUCTURE: Start structuring
|
||||
- GOTO_SECTION_WRITING: Start writing sections
|
||||
- GOTO_PAPER_REVIEW: Start review
|
||||
- GOTO_LATEX_GENERATION: Generate LaTeX
|
||||
|
||||
intro_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.7
|
||||
system_prompt: |
|
||||
Welcome users to the scientific paper writer.
|
||||
Explain the process and guide them to start with discovery.
|
||||
When ready, emit "GOTO_DISCOVERY:" to proceed.
|
||||
|
||||
discovery_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.7
|
||||
system_prompt: |
|
||||
You are the discovery agent. Gather paper requirements.
|
||||
Ask about topic, length, audience, publication, and format.
|
||||
Store details in context.
|
||||
|
||||
For specific questions, emit:
|
||||
- ROUTE_ASK_TOPIC: Ask about topic
|
||||
- ROUTE_ASK_LENGTH: Ask about length
|
||||
- ROUTE_ASK_AUDIENCE: Ask about audience
|
||||
|
||||
ask_topic_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Ask the user about their paper topic. Be specific and helpful."
|
||||
|
||||
ask_length_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Ask about the desired paper length (words/pages)."
|
||||
|
||||
ask_audience_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Ask about the target audience for the paper."
|
||||
|
||||
brainstorming_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.8
|
||||
system_prompt: "Help brainstorm ideas for the scientific paper based on the topic."
|
||||
|
||||
vetting_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Evaluate and refine the paper ideas, checking for feasibility and originality."
|
||||
|
||||
structure_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Create a detailed outline and structure for the paper."
|
||||
|
||||
section_writing_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Write individual sections of the paper based on the outline."
|
||||
|
||||
paper_review_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Review the complete paper for coherence, accuracy, and quality."
|
||||
|
||||
latex_generation_agent:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
system_prompt: "Generate LaTeX code for the final paper with proper formatting."
|
||||
|
||||
# Routes configuration using LangGraph with message router
|
||||
routes:
|
||||
main:
|
||||
type: graph
|
||||
entry_point: start
|
||||
|
||||
nodes:
|
||||
start:
|
||||
type: START
|
||||
|
||||
end:
|
||||
type: END
|
||||
|
||||
# Main message router with user-defined routing patterns
|
||||
router:
|
||||
type: message_router
|
||||
rules:
|
||||
# Main workflow routing patterns
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_INTRO"
|
||||
target: intro
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_DISCOVERY"
|
||||
target: discovery
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_BRAINSTORMING"
|
||||
target: brainstorming
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_VETTING"
|
||||
target: vetting
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_STRUCTURE"
|
||||
target: structure
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_SECTION_WRITING"
|
||||
target: section_writing
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_PAPER_REVIEW"
|
||||
target: paper_review
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "GOTO_LATEX_GENERATION"
|
||||
target: latex_generation
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
# Discovery sub-routing patterns
|
||||
- match_type: prefix
|
||||
pattern: "ROUTE_ASK_TOPIC"
|
||||
target: ask_topic
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "ROUTE_ASK_LENGTH"
|
||||
target: ask_length
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
- match_type: prefix
|
||||
pattern: "ROUTE_ASK_AUDIENCE"
|
||||
target: ask_audience
|
||||
extract_message: true
|
||||
separator: ":"
|
||||
|
||||
# Default to workflow controller
|
||||
- match_type: suffix
|
||||
pattern: ""
|
||||
target: workflow_controller
|
||||
extract_message: false
|
||||
|
||||
# Agent nodes
|
||||
workflow_controller:
|
||||
type: AGENT
|
||||
agent: workflow_controller
|
||||
|
||||
intro:
|
||||
type: AGENT
|
||||
agent: intro_agent
|
||||
|
||||
discovery:
|
||||
type: AGENT
|
||||
agent: discovery_agent
|
||||
|
||||
ask_topic:
|
||||
type: AGENT
|
||||
agent: ask_topic_agent
|
||||
|
||||
ask_length:
|
||||
type: AGENT
|
||||
agent: ask_length_agent
|
||||
|
||||
ask_audience:
|
||||
type: AGENT
|
||||
agent: ask_audience_agent
|
||||
|
||||
brainstorming:
|
||||
type: AGENT
|
||||
agent: brainstorming_agent
|
||||
|
||||
vetting:
|
||||
type: AGENT
|
||||
agent: vetting_agent
|
||||
|
||||
structure:
|
||||
type: AGENT
|
||||
agent: structure_agent
|
||||
|
||||
section_writing:
|
||||
type: AGENT
|
||||
agent: section_writing_agent
|
||||
|
||||
paper_review:
|
||||
type: AGENT
|
||||
agent: paper_review_agent
|
||||
|
||||
latex_generation:
|
||||
type: AGENT
|
||||
agent: latex_generation_agent
|
||||
|
||||
# Edge definitions
|
||||
edges:
|
||||
# Start to router
|
||||
- source: start
|
||||
target: router
|
||||
|
||||
# Router to all possible targets based on next_node
|
||||
- source: router
|
||||
target: workflow_controller
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: workflow_controller
|
||||
|
||||
- source: router
|
||||
target: intro
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: intro
|
||||
|
||||
- source: router
|
||||
target: discovery
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: discovery
|
||||
|
||||
- source: router
|
||||
target: ask_topic
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: ask_topic
|
||||
|
||||
- source: router
|
||||
target: ask_length
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: ask_length
|
||||
|
||||
- source: router
|
||||
target: ask_audience
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: ask_audience
|
||||
|
||||
- source: router
|
||||
target: brainstorming
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: brainstorming
|
||||
|
||||
- source: router
|
||||
target: vetting
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: vetting
|
||||
|
||||
- source: router
|
||||
target: structure
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: structure
|
||||
|
||||
- source: router
|
||||
target: section_writing
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: section_writing
|
||||
|
||||
- source: router
|
||||
target: paper_review
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: paper_review
|
||||
|
||||
- source: router
|
||||
target: latex_generation
|
||||
condition:
|
||||
type: context_value
|
||||
key: next_node
|
||||
value: latex_generation
|
||||
|
||||
# All agents return to router
|
||||
- source: workflow_controller
|
||||
target: router
|
||||
|
||||
- source: intro
|
||||
target: router
|
||||
|
||||
- source: discovery
|
||||
target: router
|
||||
|
||||
- source: ask_topic
|
||||
target: router
|
||||
|
||||
- source: ask_length
|
||||
target: router
|
||||
|
||||
- source: ask_audience
|
||||
target: router
|
||||
|
||||
- source: brainstorming
|
||||
target: router
|
||||
|
||||
- source: vetting
|
||||
target: router
|
||||
|
||||
- source: structure
|
||||
target: router
|
||||
|
||||
- source: section_writing
|
||||
target: router
|
||||
|
||||
- source: paper_review
|
||||
target: router
|
||||
|
||||
- source: latex_generation
|
||||
target: router
|
||||
|
||||
# Output configuration
|
||||
publications:
|
||||
- __output__
|
||||
|
||||
merges:
|
||||
- sources: [__input__]
|
||||
target: main
|
||||
+102
-38
@@ -33,7 +33,9 @@ class ToolAgent(Agent):
|
||||
stream architecture, supporting both built-in and custom tools.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, config: dict[str, Any], template_renderer: TemplateRenderer):
|
||||
def __init__(
|
||||
self, name: str, config: dict[str, Any], template_renderer: TemplateRenderer
|
||||
):
|
||||
"""
|
||||
Initialize a reactive tool agent.
|
||||
|
||||
@@ -74,20 +76,26 @@ class ToolAgent(Agent):
|
||||
# Validate tools
|
||||
self._validate_tools()
|
||||
|
||||
logger.info("Initialized reactive tool agent %s with tools: %s", name, self.tools)
|
||||
logger.info(
|
||||
"Initialized reactive tool agent %s with tools: %s", name, self.tools
|
||||
)
|
||||
|
||||
def _validate_tools(self) -> None:
|
||||
"""Validate the configured tools."""
|
||||
for tool in self.tools:
|
||||
if isinstance(tool, str):
|
||||
if tool not in self.builtin_tools and not self.allow_shell:
|
||||
raise AgentCreationError(f"Unknown tool '{tool}' and shell execution disabled")
|
||||
raise AgentCreationError(
|
||||
f"Unknown tool '{tool}' and shell execution disabled"
|
||||
)
|
||||
elif isinstance(tool, dict):
|
||||
if "name" not in tool:
|
||||
raise AgentCreationError("Tool configuration must include 'name'")
|
||||
# Allow inline code tools
|
||||
if "code" in tool and not isinstance(tool["code"], str):
|
||||
raise AgentCreationError(f"Tool code must be a string: {tool['name']}")
|
||||
raise AgentCreationError(
|
||||
f"Tool code must be a string: {tool['name']}"
|
||||
)
|
||||
else:
|
||||
raise AgentCreationError(f"Invalid tool configuration: {tool}")
|
||||
|
||||
@@ -140,7 +148,9 @@ class ToolAgent(Agent):
|
||||
|
||||
return None
|
||||
|
||||
async def process_message(self, message: str, context: Optional[dict[str, Any]] = None) -> str:
|
||||
async def process_message(
|
||||
self, message: str, context: Optional[dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Process a message by executing the appropriate tool.
|
||||
|
||||
@@ -167,18 +177,26 @@ class ToolAgent(Agent):
|
||||
effective_context = {**self.context}
|
||||
|
||||
# Special case: if we have exactly one tool with inline code, execute it directly
|
||||
if len(self.tools) == 1 and isinstance(self.tools[0], dict) and "code" in self.tools[0]:
|
||||
if (
|
||||
len(self.tools) == 1
|
||||
and isinstance(self.tools[0], dict)
|
||||
and "code" in self.tools[0]
|
||||
):
|
||||
tool_config = self.tools[0]
|
||||
tool_args = {
|
||||
"input_data": message,
|
||||
"message": message,
|
||||
}
|
||||
result = await self._execute_tool(tool_config["name"], tool_args, effective_context)
|
||||
result = await self._execute_tool(
|
||||
tool_config["name"], tool_args, effective_context
|
||||
)
|
||||
return str(result)
|
||||
|
||||
# Check if message looks like JSON but might be invalid
|
||||
message_stripped = message.strip()
|
||||
looks_like_json = message_stripped.startswith("{") and message_stripped.endswith("}")
|
||||
looks_like_json = message_stripped.startswith(
|
||||
"{"
|
||||
) and message_stripped.endswith("}")
|
||||
|
||||
# Try to extract JSON tool request from message
|
||||
tool_request = self._extract_json_from_message(message)
|
||||
@@ -189,7 +207,9 @@ class ToolAgent(Agent):
|
||||
|
||||
if tool_name:
|
||||
# Execute the tool
|
||||
result = await self._execute_tool(tool_name, tool_args, effective_context)
|
||||
result = await self._execute_tool(
|
||||
tool_name, tool_args, effective_context
|
||||
)
|
||||
return str(result)
|
||||
elif looks_like_json:
|
||||
# Message looks like JSON but couldn't be parsed - report as invalid JSON
|
||||
@@ -230,7 +250,9 @@ class ToolAgent(Agent):
|
||||
|
||||
# Check if it's an inline Python code tool
|
||||
if tool_name in self.python_tools:
|
||||
result = await self._execute_python_code(self.python_tools[tool_name], tool_args, context)
|
||||
result = await self._execute_python_code(
|
||||
self.python_tools[tool_name], tool_args, context
|
||||
)
|
||||
return result
|
||||
|
||||
# Check if it's a built-in tool
|
||||
@@ -255,7 +277,9 @@ class ToolAgent(Agent):
|
||||
# Basic safety checks
|
||||
dangerous_commands = ["rm", "del", "format", "shutdown", "reboot", "kill"]
|
||||
if any(cmd in command.lower() for cmd in dangerous_commands):
|
||||
raise ExecutionError(f"Dangerous command '{command}' blocked in safe mode")
|
||||
raise ExecutionError(
|
||||
f"Dangerous command '{command}' blocked in safe mode"
|
||||
)
|
||||
|
||||
# Build command with arguments
|
||||
cmd_parts = [command]
|
||||
@@ -264,20 +288,30 @@ class ToolAgent(Agent):
|
||||
|
||||
try:
|
||||
# Execute with timeout
|
||||
process = await asyncio.create_subprocess_exec(*cmd_parts, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd_parts, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=self.timeout)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(), timeout=self.timeout
|
||||
)
|
||||
|
||||
if process.returncode != 0:
|
||||
error_msg = stderr.decode().strip()
|
||||
raise ExecutionError(f"Command failed with code {process.returncode}: {error_msg}")
|
||||
raise ExecutionError(
|
||||
f"Command failed with code {process.returncode}: {error_msg}"
|
||||
)
|
||||
|
||||
return stdout.decode().strip()
|
||||
|
||||
except asyncio.TimeoutError as timeout_err:
|
||||
raise ExecutionError(f"Command '{command}' timed out after {self.timeout} seconds") from timeout_err
|
||||
raise ExecutionError(
|
||||
f"Command '{command}' timed out after {self.timeout} seconds"
|
||||
) from timeout_err
|
||||
|
||||
async def _execute_python_code(self, code: str, args: dict[str, Any], context: Optional[dict[str, Any]]) -> str:
|
||||
async def _execute_python_code(
|
||||
self, code: str, args: dict[str, Any], context: Optional[dict[str, Any]]
|
||||
) -> str:
|
||||
"""Execute inline Python code safely."""
|
||||
# Create a restricted environment for code execution
|
||||
safe_globals = {
|
||||
@@ -324,7 +358,9 @@ class ToolAgent(Agent):
|
||||
}
|
||||
|
||||
# Debug: Check context before exec
|
||||
context_id_before = id(local_vars["context"]) if "context" in local_vars else None
|
||||
context_id_before = (
|
||||
id(local_vars["context"]) if "context" in local_vars else None
|
||||
)
|
||||
writing_stage_before = (
|
||||
local_vars["context"].get("writing_stage")
|
||||
if "context" in local_vars and isinstance(local_vars["context"], dict)
|
||||
@@ -335,8 +371,10 @@ class ToolAgent(Agent):
|
||||
safe_globals["json"] = __import__("json") # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
# Execute the code in the restricted environment
|
||||
logger.debug(f"Before exec - context: {local_vars.get('context')}")
|
||||
# Execute the code in a shared environment so helper functions see locals
|
||||
exec_env = dict(safe_globals)
|
||||
exec_env.update(local_vars)
|
||||
logger.debug(f"Before exec - context: {exec_env.get('context')}")
|
||||
|
||||
# Redirect stderr to suppress debug print statements unless logging is DEBUG level
|
||||
import io
|
||||
@@ -344,23 +382,25 @@ class ToolAgent(Agent):
|
||||
import sys
|
||||
|
||||
# Check if we should suppress output (when root logger level > DEBUG)
|
||||
should_suppress = log_module.getLogger().getEffectiveLevel() > log_module.DEBUG
|
||||
should_suppress = (
|
||||
log_module.getLogger().getEffectiveLevel() > log_module.DEBUG
|
||||
)
|
||||
|
||||
if should_suppress:
|
||||
# Redirect stderr to suppress print(..., file=sys.stderr) statements
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = io.StringIO()
|
||||
try:
|
||||
exec(code, safe_globals, local_vars)
|
||||
exec(code, exec_env, exec_env)
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
else:
|
||||
exec(code, safe_globals, local_vars)
|
||||
exec(code, exec_env, exec_env)
|
||||
|
||||
logger.debug(f"After exec - context: {local_vars.get('context')}")
|
||||
logger.debug(f"After exec - context: {exec_env.get('context')}")
|
||||
|
||||
# Check for context updates and save globally
|
||||
context_after = local_vars.get("context")
|
||||
context_after = exec_env.get("context")
|
||||
if context and context_after and isinstance(context_after, dict):
|
||||
global _CONTEXT_UPDATES
|
||||
_CONTEXT_UPDATES.append(context_after)
|
||||
@@ -369,13 +409,13 @@ class ToolAgent(Agent):
|
||||
# The context may have been modified in place by the exec'd code
|
||||
# No need to update it since it's the same object passed by reference
|
||||
# Get the result from the execution
|
||||
result = local_vars.get("result")
|
||||
result = exec_env.get("result")
|
||||
|
||||
if result is None:
|
||||
# Check if there's a return value stored differently
|
||||
for key in ["output", "response", "answer"]:
|
||||
if key in local_vars:
|
||||
result = local_vars[key]
|
||||
if key in exec_env:
|
||||
result = exec_env[key]
|
||||
break
|
||||
|
||||
return str(result) if result is not None else ""
|
||||
@@ -459,13 +499,17 @@ class ToolAgent(Agent):
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(method, url, headers=headers, json=data, timeout=self.timeout) as response:
|
||||
async with session.request(
|
||||
method, url, headers=headers, json=data, timeout=self.timeout
|
||||
) as response:
|
||||
content = await response.text()
|
||||
return f"Status: {response.status}\n\n{content}"
|
||||
except Exception as e:
|
||||
raise ExecutionError(f"HTTP request failed: {e}") from e
|
||||
|
||||
async def _file_read_tool(self, args: dict[str, Any], context: Optional[dict[str, Any]]) -> str:
|
||||
async def _file_read_tool(
|
||||
self, args: dict[str, Any], context: Optional[dict[str, Any]]
|
||||
) -> str:
|
||||
"""File reading tool."""
|
||||
filepath = args.get("file", "")
|
||||
if "args" in args and args["args"]:
|
||||
@@ -479,7 +523,9 @@ class ToolAgent(Agent):
|
||||
if ".." in filepath:
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
# Block absolute paths unless in unsafe mode
|
||||
if filepath.startswith("/") and not (context and context.get("_unsafe_mode", False)):
|
||||
if filepath.startswith("/") and not (
|
||||
context and context.get("_unsafe_mode", False)
|
||||
):
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
|
||||
try:
|
||||
@@ -544,7 +590,10 @@ class ToolAgent(Agent):
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
|
||||
# Check if path escapes working directory
|
||||
path_escapes_working_dir = not absolute_path.startswith(working_dir + os.sep) and absolute_path != working_dir
|
||||
path_escapes_working_dir = (
|
||||
not absolute_path.startswith(working_dir + os.sep)
|
||||
and absolute_path != working_dir
|
||||
)
|
||||
|
||||
if not unsafe_mode:
|
||||
# Caller didn't opt-in to unsafe_mode: enforce working directory restriction
|
||||
@@ -564,7 +613,9 @@ class ToolAgent(Agent):
|
||||
else:
|
||||
# Caller opted-in to unsafe_mode
|
||||
if path_escapes_working_dir and self.safe_mode:
|
||||
logger.debug("Unsafe mode: allowing path outside working dir: %s", filepath)
|
||||
logger.debug(
|
||||
"Unsafe mode: allowing path outside working dir: %s", filepath
|
||||
)
|
||||
|
||||
if is_absolute and self.safe_mode:
|
||||
# Allow absolute paths when caller provides unsafe_mode=True
|
||||
@@ -630,11 +681,15 @@ class ToolAgent(Agent):
|
||||
existing_lines.insert(line_idx, formatted_content)
|
||||
insert_location = line_idx + 1
|
||||
else:
|
||||
raise ExecutionError(f"Invalid position '{position}'. Use 'start', 'end', or line number.")
|
||||
raise ExecutionError(
|
||||
f"Invalid position '{position}'. Use 'start', 'end', or line number."
|
||||
)
|
||||
|
||||
return existing_lines, insert_location
|
||||
|
||||
async def _file_write_tool(self, args: dict[str, Any], context: Optional[dict[str, Any]]) -> str:
|
||||
async def _file_write_tool(
|
||||
self, args: dict[str, Any], context: Optional[dict[str, Any]]
|
||||
) -> str:
|
||||
"""File writing tool with support for write, append, and insert modes."""
|
||||
filepath = args.get("file", "")
|
||||
content = args.get("content", "")
|
||||
@@ -668,12 +723,19 @@ class ToolAgent(Agent):
|
||||
|
||||
if mode == "insert":
|
||||
# Insert mode at specified position
|
||||
existing_lines, insert_location = self._handle_insert_position(filepath, content, position)
|
||||
existing_lines, insert_location = self._handle_insert_position(
|
||||
filepath, content, position
|
||||
)
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.writelines(existing_lines)
|
||||
return f"Successfully inserted {len(content)} characters " f"at line {insert_location} in {filepath}"
|
||||
return (
|
||||
f"Successfully inserted {len(content)} characters "
|
||||
f"at line {insert_location} in {filepath}"
|
||||
)
|
||||
|
||||
raise ExecutionError(f"Invalid mode '{mode}'. Use 'w' (write), 'a' (append), or 'insert'.")
|
||||
raise ExecutionError(
|
||||
f"Invalid mode '{mode}'. Use 'w' (write), 'a' (append), or 'insert'."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("File write failed for %s: %s", filepath, e)
|
||||
raise ExecutionError(f"File write failed: {e}") from e
|
||||
@@ -682,7 +744,9 @@ class ToolAgent(Agent):
|
||||
"""Get the capabilities of the tool agent."""
|
||||
capabilities = ["tool-execution", "command-execution"]
|
||||
|
||||
if "http_request" in self.tools or "http_request" in [t.get("name") for t in self.tools if isinstance(t, dict)]:
|
||||
if "http_request" in self.tools or "http_request" in [
|
||||
t.get("name") for t in self.tools if isinstance(t, dict)
|
||||
]:
|
||||
capabilities.append("http-requests")
|
||||
|
||||
dict_tool_names = [t.get("name") for t in self.tools if isinstance(t, dict)]
|
||||
|
||||
@@ -345,12 +345,22 @@ class PureLangGraph:
|
||||
f"_execute_from_node called with node_name='{node_name}', message type={type(message)}, depth={depth}"
|
||||
)
|
||||
|
||||
# Prevent infinite recursion - use a much lower limit for interactive graphs
|
||||
max_depth = 20 # Maximum graph traversal depth
|
||||
# Prevent infinite recursion - but allow deeper traversal for complex workflows
|
||||
# For workflows with many sections (like scientific paper writer), we need high limits
|
||||
# Each section requires ~20 node visits, so 50 sections = 1000+ visits
|
||||
# Use a generous limit that allows complex workflows to complete
|
||||
max_depth = max(2000, len(self.nodes) * 50)
|
||||
|
||||
if depth > max_depth:
|
||||
self.logger.warning(
|
||||
f"Maximum recursion depth {max_depth} exceeded. Returning current output."
|
||||
)
|
||||
import sys
|
||||
|
||||
print(
|
||||
f"MAX_DEPTH_EXCEEDED node={node_name} depth={depth} max={max_depth}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return message
|
||||
|
||||
# Track node visits with message fingerprints to detect actual loops
|
||||
@@ -369,13 +379,39 @@ class PureLangGraph:
|
||||
|
||||
# Detect when a specific non-router node is visited with the same message twice
|
||||
# This indicates an actual loop, not just a legitimate revisit with a different message
|
||||
# EXCEPTION: When auto_finish_active is True, we allow repeated visits to support
|
||||
# the multi-section writing workflow where sections are processed sequentially
|
||||
if "router" not in node_name.lower():
|
||||
if self._node_message_visits[visit_key] > 1:
|
||||
self.logger.debug(
|
||||
f"Node '{node_name}' visited {self._node_message_visits[visit_key]} times "
|
||||
f"with the same message. Stopping execution to return output to user."
|
||||
)
|
||||
return message
|
||||
# Check if auto_finish_active is set in context - if so, bypass loop detection
|
||||
state = self.state_manager.get_state()
|
||||
auto_finish_active = False
|
||||
if hasattr(state, "metadata"):
|
||||
# Check direct metadata first
|
||||
auto_finish_active = state.metadata.get("auto_finish_active", False)
|
||||
# Also check nested context
|
||||
if not auto_finish_active and "context" in state.metadata:
|
||||
auto_finish_active = state.metadata.get("context", {}).get(
|
||||
"auto_finish_active", False
|
||||
)
|
||||
|
||||
if auto_finish_active:
|
||||
self.logger.debug(
|
||||
f"Node '{node_name}' visited {self._node_message_visits[visit_key]} times "
|
||||
f"with same message, but auto_finish_active=True - continuing execution."
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
f"Node '{node_name}' visited {self._node_message_visits[visit_key]} times "
|
||||
f"with the same message. Stopping execution to return output to user."
|
||||
)
|
||||
import sys
|
||||
|
||||
print(
|
||||
f"LOOP_STOP node={node_name} visits={self._node_message_visits[visit_key]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return message
|
||||
|
||||
# Additional safeguard: track consecutive router-agent cycles
|
||||
if not hasattr(self, "_execution_path"):
|
||||
@@ -389,6 +425,7 @@ class PureLangGraph:
|
||||
# Check for router-agent ping-pong pattern
|
||||
# A true ping-pong is when the SAME agent is visited twice with router in between
|
||||
# Pattern we're looking for: router -> agentX -> router -> agentX (same agent)
|
||||
# EXCEPTION: When auto_finish_active is True, we allow this pattern for workflow progression
|
||||
if len(self._execution_path) >= 4:
|
||||
recent = self._execution_path[-4:]
|
||||
# Check if pattern is: router -> agent -> router -> SAME agent
|
||||
@@ -396,13 +433,37 @@ class PureLangGraph:
|
||||
if is_router == [True, False, True, False]:
|
||||
# Check if the two agents are the same
|
||||
if recent[1] == recent[3]:
|
||||
self.logger.debug(
|
||||
f"Detected router-agent ping-pong loop with same agent: {recent}. "
|
||||
f"Stopping execution to return output."
|
||||
)
|
||||
if self._execution_path:
|
||||
self._execution_path.pop()
|
||||
return message
|
||||
# Check if auto_finish_active is set - if so, bypass ping-pong detection
|
||||
state = self.state_manager.get_state()
|
||||
auto_finish_active = False
|
||||
if hasattr(state, "metadata"):
|
||||
auto_finish_active = state.metadata.get(
|
||||
"auto_finish_active", False
|
||||
)
|
||||
if not auto_finish_active and "context" in state.metadata:
|
||||
auto_finish_active = state.metadata.get("context", {}).get(
|
||||
"auto_finish_active", False
|
||||
)
|
||||
|
||||
if auto_finish_active:
|
||||
self.logger.debug(
|
||||
f"Detected router-agent pattern with same agent: {recent}, "
|
||||
f"but auto_finish_active=True - continuing execution."
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
f"Detected router-agent ping-pong loop with same agent: {recent}. "
|
||||
f"Stopping execution to return output."
|
||||
)
|
||||
import sys
|
||||
|
||||
print(
|
||||
f"PING_PONG_DETECTED recent={recent} auto_finish={auto_finish_active}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self._execution_path:
|
||||
self._execution_path.pop()
|
||||
return message
|
||||
|
||||
# Handle special nodes
|
||||
if node_name == "start":
|
||||
@@ -498,6 +559,8 @@ class PureLangGraph:
|
||||
"SET_",
|
||||
"CMD_",
|
||||
"DISCOVERY_RESPONSE",
|
||||
"AUTO_SECTIONS_COMPLETE",
|
||||
"COMMAND_OUTPUT",
|
||||
]
|
||||
)
|
||||
if not has_routing_command:
|
||||
|
||||
Reference in New Issue
Block a user