Nikita Miroshnichenko commited on
Commit
f988705
·
unverified ·
2 Parent(s): da9a42e b033223

Merge pull request #1 from Lebaranto/codex/refactor-planner-and-executor-prompts

Browse files
Files changed (4) hide show
  1. src/nodes.py +305 -185
  2. src/prompts/prompts.py +46 -62
  3. src/schemas.py +11 -21
  4. src/utils/utils.py +58 -1
src/nodes.py CHANGED
@@ -1,161 +1,233 @@
1
  import os
 
 
2
  from state import AgentState
3
  from tools.tools import preprocess_files
4
  from langgraph.prebuilt import ToolNode
5
- from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
6
- from prompts.prompts import SYSTEM_PROMPT_PLANNER, SYSTEM_EXECUTOR_PROMPT, COMPLEXITY_ASSESSOR_PROMPT, CRITIC_PROMPT
 
 
 
 
 
 
7
  from config import llm, TOOLS, planner_llm, llm_with_tools
8
  from schemas import PlannerPlan, ComplexityLevel, CritiqueFeedback, ExecutionReport, ToolExecution
9
- from utils.utils import format_final_answer, clean_message_history
10
-
11
- def query_input(state : AgentState) -> AgentState:
12
- print("=== USER QUERY TRANSFERED TO AGENT ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  files = state.get("files", [])
15
  if files:
16
- print(f"Processing {len(files)} files:")
17
  file_info = preprocess_files(files)
18
-
19
  for file_path, info in file_info.items():
20
- print(f" - {file_path}: {info['type']} ({info['size']} bytes) -> {info['suggested_tool']}")
 
 
 
 
 
 
 
21
 
22
  state["file_contents"] = file_info
23
  file_context = "\n\n=== AVAILABLE FILES FOR ANALYSIS ===\n"
24
  for file_path, info in file_info.items():
25
  filename = os.path.basename(file_path)
26
  file_context += f"File: {filename}\n"
27
- file_context += f" - Type: {info['type']}\n"
28
  file_context += f" - Size: {info['size']} bytes\n"
29
  file_context += f" - Suggested tool: {info['suggested_tool']}\n"
30
  if info.get("preview"):
31
  file_context += f" - Preview: {info['preview']}\n"
32
  file_context += "\n"
33
-
34
- # Добавляем инструкции по работе с файлами
35
  file_context += "IMPORTANT: Use the suggested tools to analyze these files before processing their data.\n"
36
  file_context += "File paths are available in the agent state and can be passed directly to analysis tools.\n"
37
-
38
  original_query = state.get("query", "")
39
  state["query"] = original_query + file_context
 
 
40
  return state
41
 
42
 
43
- def planner(state : AgentState) -> AgentState:
 
 
 
44
  sys_stack = [
45
- SystemMessage(content=SYSTEM_PROMPT_PLANNER.strip()),
46
- HumanMessage(content=state["query"]),
47
- ]
48
  plan: PlannerPlan = planner_llm.invoke(sys_stack)
49
-
50
- print("=== GENERATED PLAN ===")
51
- return {"messages" : sys_stack + state["messages"],
52
- "plan": plan,
53
- "current_step ": 0,
54
- "reasoning_done": False}
 
 
55
 
56
 
57
  def agent(state: AgentState) -> AgentState:
58
-
59
- """
60
- sys_msg = SystemMessage(
61
- content=SYSTEM_EXECUTOR_PROMPT.strip().format(
62
- plan=json.dumps(state["plan"], indent=2)
63
- )
64
- )
65
- """
66
  current_step = state.get("current_step", 0)
67
  reasoning_done = state.get("reasoning_done", False)
68
- plan = state.get("plan", {})
69
- steps = state["plan"].steps
70
 
71
- print(f"=== AGENT DEBUG ===")
72
- print(f"Current step: {current_step}")
73
- print(f"Reasoning done: {reasoning_done}")
74
- print(f"Plan exists: {plan is not None}")
75
- print(f"Total steps in plan: {len(plan.steps) if plan else 'No plan'}")
76
-
77
- if not plan or not hasattr(plan, 'steps') or not plan.steps:
78
- print("ERROR: No valid plan found!")
79
  return {
80
- "messages": state["messages"] + [AIMessage(content="No valid plan available. <FINAL_ANSWER>")],
81
- "reasoning_done": False
82
  }
83
-
84
  steps = plan.steps
85
-
86
- if current_step >= len(steps):
87
- print("All plan steps completed, moving to finalization")
 
 
 
 
 
 
 
 
 
 
88
  return {
89
- "messages": state["messages"] + [AIMessage(content="All steps completed. <FINAL_ANSWER>")],
90
- "reasoning_done": False
91
  }
92
 
93
  current_step_info = steps[current_step]
94
- print(f"Executing step {current_step + 1}: {current_step_info.description}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  if not reasoning_done:
 
 
 
 
 
 
 
 
 
 
97
 
98
- # ✅ ДОБАВЛЕНО: Специальный контекст для файлов
99
- file_context = ""
100
- file_contents = state.get("file_contents", {})
101
- if file_contents:
102
- file_context = "\n\nAVAILABLE FILES IN CURRENT SESSION:\n"
103
- for filepath, info in file_contents.items():
104
- filename = os.path.basename(filepath)
105
- file_context += f"- {filename}: {info['type']} file, suggested tool: {info['suggested_tool']}\n"
106
- file_context += f" Path: {filepath}\n"
107
-
108
- reasoning_prompt = f"""
109
- {SYSTEM_EXECUTOR_PROMPT}
110
-
111
- CURRENT TASK: You must perform reasoning for step {current_step + 1}.
112
-
113
- STEP INFO: {current_step_info}\n\n
114
 
115
- FILE CONTEXT: {file_contents}
116
-
117
- CRITICAL: You MUST output your reasoning in <REASONING> tags, but DO NOT call any tools yet.
118
- Explain what you need to do and why, then end your response.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- REASONING IS IMPERATIVE BEFORE ANY TOOL CALLS.
121
- """
 
 
 
 
 
 
 
122
 
123
- sys_msg = SystemMessage(content = reasoning_prompt)
124
- stack = [sys_msg] + state["messages"]
 
 
 
 
 
 
125
 
126
- step = llm.invoke(stack)
127
- print("=== REASONING STEP ===")
128
- print(step.content)
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
- return {
131
- "messages" : state["messages"] + [step],
132
- "reasoning_done" : True
133
- }
134
-
135
- else:
136
- tool_prompt = f"""
137
- Now execute the tool for step {current_step + 1}.
138
-
139
- You have already done the reasoning. Now call the appropriate tool with the correct parameters.
140
- Available file paths: {list(state.get("file_contents", {}).keys())}\n
141
- IMPORTANT NOTE: IF YOU DECIDED TO USE safe_code_run, MAKE SURE TO FINISH CALCULATIONS WITH print() or saving to a variable NAMED 'result' so that the output can be captured!
142
- AVAILABLE TOOLS: {', '.join([tool.name for tool in TOOLS])}
143
- """
144
-
145
- sys_msg = SystemMessage(content=tool_prompt)
146
- stack = [sys_msg] + state["messages"] # Берем последние сообщения включая reasoning
147
-
148
- # Используем модель С инструментами для выполнения
149
- step = llm_with_tools.invoke(stack)
150
- print("=== TOOL EXECUTION ===")
151
- print(f"Tool calls: {step.tool_calls}")
152
-
153
- return {
154
- "messages": state["messages"] + [step],
155
- "current_step": current_step + 1 if step.tool_calls else current_step,
156
- "reasoning_done": False # Сбрасываем для следующего шага
157
- }
158
-
159
  def should_continue(state : AgentState) -> bool:
160
 
161
  last_message = state["messages"][-1]
@@ -185,19 +257,45 @@ def should_continue(state : AgentState) -> bool:
185
  class DebuggingToolNode(ToolNode):
186
  def __init__(self, tools):
187
  super().__init__(tools)
188
-
189
  def __call__(self, state):
190
- print("=== TOOL EXECUTION STARTED ===")
191
- result = super().__call__(state)
192
- print("=== TOOL EXECUTION COMPLETED ===")
193
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
 
196
 
197
  def enhanced_finalizer(state: AgentState) -> AgentState:
198
  """Generate comprehensive execution report for critic evaluation."""
199
- print("=== GENERATING EXECUTION REPORT ===")
200
-
201
  # Extract tool execution information
202
  tools_executed = []
203
  data_sources = []
@@ -222,20 +320,22 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
222
  plan = state.get("plan")
223
  approach_used = "Direct execution"
224
  assumptions_made = []
225
-
 
226
  if plan:
227
- approach_used = f"{plan.task_type} approach with {len(plan.steps)} steps"
228
  assumptions_made = plan.assumptions
 
229
 
230
  # Generate structured report (КОСТЫЛЬ ЗДЕСЬ!)
231
  report_generator_prompt = f"""
232
  Generate a comprehensive execution report for the following query processing:
233
 
234
  ORIGINAL QUERY: {state['query']}
235
-
236
  EXECUTION CONTEXT:
237
  - Complexity Level: {state.get('complexity_assessment', {}).level}
238
- - Plan Used: {plan if plan else {}}
239
  - Tools Executed: {tools_executed}
240
  - Available Files: {list(state.get('file_contents', {}).keys())}
241
 
@@ -262,13 +362,18 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
262
  HumanMessage(content="Generate the execution report.")
263
  ])
264
 
265
- print(f"Report generated - Confidence: {execution_report.confidence_level}")
266
- print(f"Key findings: {len(execution_report.key_findings)}")
267
- print(f"Data sources: {len(execution_report.data_sources)}")
268
-
 
 
 
 
269
  # Format final answer for user
270
  formatted_answer = format_final_answer(execution_report, state.get('complexity_assessment', {}))
271
- print(execution_report)
 
272
  return {
273
  "execution_report": execution_report,
274
  "final_answer": formatted_answer
@@ -277,23 +382,25 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
277
 
278
  def simple_executor(state: AgentState) -> AgentState:
279
  """Handle simple queries directly without planning."""
280
- print("=== SIMPLE EXECUTION ===")
281
-
282
  # For simple queries, use the LLM with tools directly
283
  simple_prompt = f"""
284
  Answer this simple query directly and efficiently: {state['query']}
285
-
286
- You have access to tools if needed, but try to answer directly when possible.
287
- If you need files, they are available at: {list(state.get('file_contents', {}).keys())}
288
-
289
- Provide a clear, concise answer.
290
  """
291
-
292
  response = llm_with_tools.invoke([
293
  SystemMessage(content=simple_prompt),
294
  HumanMessage(content=state['query'])
295
  ])
296
 
 
 
 
297
  return {
298
  "messages": state["messages"] + [response],
299
  "final_answer": response.content
@@ -312,8 +419,8 @@ def should_use_planning(state: AgentState) -> str:
312
 
313
  def critic_evaluator(state: AgentState) -> AgentState:
314
  """Enhanced critic that evaluates execution reports."""
315
- print("=== ENHANCED ANSWER CRITIQUE ===")
316
-
317
  report = state.get("execution_report")
318
  critic_llm = llm.with_structured_output(CritiqueFeedback)
319
 
@@ -333,15 +440,22 @@ def critic_evaluator(state: AgentState) -> AgentState:
333
  HumanMessage(content="Evaluate this execution report thoroughly.")
334
  ])
335
 
336
- print(f"Quality Score: {critique.quality_score}/10")
337
- print(f"Complete: {critique.is_complete}")
338
- print(f"Accurate: {critique.is_accurate}")
339
-
 
 
 
 
340
  if critique.errors_found:
341
- print(f"Issues found: {critique.errors_found}")
342
-
 
 
343
  if critique.needs_replanning:
344
- print(f"Replanning needed: {critique.replan_instructions}")
 
345
 
346
  return {
347
  "critique_feedback": critique,
@@ -355,64 +469,63 @@ def should_replan(state: AgentState) -> str:
355
  critique = state.get("critique_feedback")
356
  iteration_count = state.get("iteration_count", 0)
357
  max_iterations = state.get("max_iterations", 3)
358
-
359
 
360
- print(f"=== REPLAN DECISION ===")
361
- print(f"Iteration: {iteration_count}/{max_iterations}")
362
- print(f"Quality score: {critique.quality_score if critique else 'N/A'}")
363
- print(f"Needs replanning: {critique.needs_replanning if critique else 'N/A'}")
 
 
 
 
 
364
 
365
  if not critique:
366
  return "end"
367
-
368
  # Stop if max iterations reached
369
  if iteration_count >= max_iterations:
370
- print(f"Max iterations ({max_iterations}) reached. Accepting current answer.")
371
  return "end"
372
-
373
  # Accept if quality is good enough
374
  if critique.quality_score >= 7 or not critique.needs_replanning:
375
- print("Quality acceptable, ending execution")
376
  return "end"
377
-
378
  # Replan if quality is poor and we haven't exceeded max iterations
379
  if critique.needs_replanning and iteration_count < max_iterations:
380
- print("Replanning due to critic feedback...")
381
  return "replan"
382
-
383
  return "end"
384
 
385
  def replanner(state: AgentState) -> AgentState:
386
  """Create a revised plan based on critic feedback."""
387
- print("=== REPLANNING ===")
388
-
389
  critique = state["critique_feedback"]
390
  previous_plan = state.get("plan")
391
-
392
- replan_prompt = f"""
393
- {SYSTEM_PROMPT_PLANNER}
394
-
395
- REPLANNING CONTEXT:
396
- Original Query: {state['query']}
397
- Previous Plan: {previous_plan if previous_plan else {}}
398
-
399
- CRITIC FEEDBACK:
400
- - Quality Score: {critique.quality_score}/10
401
- - Issues Found: {critique.errors_found}
402
- - Missing Elements: {critique.missing_elements}
403
- - Improvement Suggestions: {critique.suggested_improvements}
404
- - Specific Instructions: {critique.replan_instructions}
405
-
406
- Create a REVISED plan that addresses these issues. Focus on fixing the identified problems.
407
- """
408
-
409
  revised_plan = planner_llm.invoke([
410
  SystemMessage(content=replan_prompt),
411
  HumanMessage(content="Create a revised plan based on the feedback.")
412
  ])
413
-
414
- print("Plan revised based on critic feedback")
415
-
416
  # Очищаем историю сообщений от неполных tool_calls
417
  current_messages = state.get("messages", [])
418
  cleaned_messages = clean_message_history(current_messages)
@@ -427,8 +540,12 @@ def replanner(state: AgentState) -> AgentState:
427
  isinstance(msg, HumanMessage)):
428
  essential_messages.append(msg)
429
 
430
- print(f"Cleaned message history: {len(current_messages)} -> {len(essential_messages)} messages")
431
-
 
 
 
 
432
  return {
433
  "plan": revised_plan,
434
  "current_step": 0,
@@ -440,21 +557,24 @@ def replanner(state: AgentState) -> AgentState:
440
 
441
  def complexity_assessor(state: AgentState) -> AgentState:
442
  """Assess query complexity and determine if planning is needed."""
443
- print("=== COMPLEXITY ASSESSMENT ===")
444
-
445
  complexity_llm = llm.with_structured_output(ComplexityLevel)
446
-
447
  assessment_message = [
448
  SystemMessage(content=COMPLEXITY_ASSESSOR_PROMPT.strip()),
449
  HumanMessage(content=f"Query: {state['query']}")
450
  ]
451
-
452
  assessment = complexity_llm.invoke(assessment_message)
453
-
454
- print(f"Complexity: {assessment.level}")
455
- print(f"Needs planning: {assessment.needs_planning}")
456
- print(f"Reasoning: {assessment.reasoning}")
457
-
 
 
 
458
  return {
459
  "complexity_assessment": assessment,
460
  "messages": state["messages"] + assessment_message
 
1
  import os
2
+ from typing import Optional
3
+
4
  from state import AgentState
5
  from tools.tools import preprocess_files
6
  from langgraph.prebuilt import ToolNode
7
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
8
+
9
+ from prompts.prompts import (
10
+ SYSTEM_PROMPT_PLANNER,
11
+ SYSTEM_EXECUTOR_PROMPT,
12
+ COMPLEXITY_ASSESSOR_PROMPT,
13
+ CRITIC_PROMPT,
14
+ )
15
  from config import llm, TOOLS, planner_llm, llm_with_tools
16
  from schemas import PlannerPlan, ComplexityLevel, CritiqueFeedback, ExecutionReport, ToolExecution
17
+ from utils.utils import (
18
+ format_final_answer,
19
+ clean_message_history,
20
+ log_stage,
21
+ log_key_values,
22
+ display_plan,
23
+ format_plan_overview,
24
+ )
25
+
26
+
27
+ def _build_planner_prompt(state: AgentState, extra_context: Optional[str] = None) -> str:
28
+ tool_catalogue = ", ".join(sorted(tool.name for tool in TOOLS))
29
+ file_paths = state.get("files", [])
30
+ file_list = ", ".join(os.path.basename(path) for path in file_paths) if file_paths else "none provided"
31
+ extra = extra_context.strip() if extra_context else "None"
32
+ return SYSTEM_PROMPT_PLANNER.format(
33
+ tool_catalogue=tool_catalogue,
34
+ file_list=file_list,
35
+ extra_context=extra,
36
+ ).strip()
37
+
38
+ def query_input(state: AgentState) -> AgentState:
39
+ log_stage("USER QUERY", icon="💡")
40
 
41
  files = state.get("files", [])
42
  if files:
43
+ log_stage("FILE PREPARATION", subtitle=f"Processing {len(files)} file(s)", icon="📁")
44
  file_info = preprocess_files(files)
45
+
46
  for file_path, info in file_info.items():
47
+ log_key_values(
48
+ [
49
+ ("path", file_path),
50
+ ("type", info["type"]),
51
+ ("size", f"{info['size']} bytes"),
52
+ ("suggested_tool", info["suggested_tool"]),
53
+ ]
54
+ )
55
 
56
  state["file_contents"] = file_info
57
  file_context = "\n\n=== AVAILABLE FILES FOR ANALYSIS ===\n"
58
  for file_path, info in file_info.items():
59
  filename = os.path.basename(file_path)
60
  file_context += f"File: {filename}\n"
61
+ file_context += f" - Type: {info['type']}\n"
62
  file_context += f" - Size: {info['size']} bytes\n"
63
  file_context += f" - Suggested tool: {info['suggested_tool']}\n"
64
  if info.get("preview"):
65
  file_context += f" - Preview: {info['preview']}\n"
66
  file_context += "\n"
67
+
 
68
  file_context += "IMPORTANT: Use the suggested tools to analyze these files before processing their data.\n"
69
  file_context += "File paths are available in the agent state and can be passed directly to analysis tools.\n"
70
+
71
  original_query = state.get("query", "")
72
  state["query"] = original_query + file_context
73
+ else:
74
+ log_key_values([("files", "none provided")])
75
  return state
76
 
77
 
78
+ def planner(state: AgentState) -> AgentState:
79
+ log_stage("PLANNING", icon="🧭")
80
+ planner_prompt = _build_planner_prompt(state)
81
+
82
  sys_stack = [
83
+ SystemMessage(content=planner_prompt),
84
+ HumanMessage(content=state["query"]),
85
+ ]
86
  plan: PlannerPlan = planner_llm.invoke(sys_stack)
87
+
88
+ display_plan(plan)
89
+ return {
90
+ "messages": state["messages"] + sys_stack,
91
+ "plan": plan,
92
+ "current_step": 0,
93
+ "reasoning_done": False,
94
+ }
95
 
96
 
97
  def agent(state: AgentState) -> AgentState:
 
 
 
 
 
 
 
 
98
  current_step = state.get("current_step", 0)
99
  reasoning_done = state.get("reasoning_done", False)
100
+ plan: Optional[PlannerPlan] = state.get("plan")
 
101
 
102
+ if not plan or not hasattr(plan, "steps"):
103
+ log_stage("PLAN VALIDATION", subtitle="Planner returned no actionable steps", icon="⚠️")
104
+ warning = AIMessage(content="No valid plan available. <FINAL_ANSWER>")
 
 
 
 
 
105
  return {
106
+ "messages": state["messages"] + [warning],
107
+ "reasoning_done": False,
108
  }
109
+
110
  steps = plan.steps
111
+ total_steps = len(steps)
112
+
113
+ if total_steps == 0:
114
+ log_stage("PLAN VALIDATION", subtitle="Plan indicates direct answer", icon="ℹ️")
115
+ direct = AIMessage(content="Plan has no steps; respond directly. <FINAL_ANSWER>")
116
+ return {
117
+ "messages": state["messages"] + [direct],
118
+ "reasoning_done": False,
119
+ }
120
+
121
+ if current_step >= total_steps:
122
+ log_stage("PLAN COMPLETE", subtitle="All steps executed", icon="✅")
123
+ completion = AIMessage(content="All plan steps completed. <FINAL_ANSWER>")
124
  return {
125
+ "messages": state["messages"] + [completion],
126
+ "reasoning_done": False,
127
  }
128
 
129
  current_step_info = steps[current_step]
130
+ log_stage(
131
+ "EXECUTION",
132
+ subtitle=f"Step {current_step + 1}/{total_steps}: {current_step_info.goal}",
133
+ icon="🤖",
134
+ )
135
+ log_key_values(
136
+ [
137
+ ("step_id", current_step_info.id),
138
+ ("tool", current_step_info.tool or "none"),
139
+ ("expected", current_step_info.expected_result),
140
+ ]
141
+ )
142
+
143
+ plan_overview = format_plan_overview(plan)
144
+ tool_catalogue = ", ".join(sorted(tool.name for tool in TOOLS))
145
+ file_contents = state.get("file_contents", {})
146
+ file_list = ", ".join(file_contents.keys()) if file_contents else "none provided"
147
+
148
+ system_message = SystemMessage(
149
+ content=SYSTEM_EXECUTOR_PROMPT.format(
150
+ plan_summary=plan.summary,
151
+ plan_overview=plan_overview,
152
+ current_step_id=current_step_info.id,
153
+ step_goal=current_step_info.goal,
154
+ step_tool=current_step_info.tool or "no tool (respond directly)",
155
+ tool_catalogue=tool_catalogue,
156
+ file_list=file_list,
157
+ ).strip()
158
+ )
159
 
160
  if not reasoning_done:
161
+ instruction = HumanMessage(
162
+ content=(
163
+ "Provide reasoning for this step inside <REASONING>...</REASONING>. "
164
+ "Do not call any tools yet."
165
+ )
166
+ )
167
+ stack = [system_message] + state["messages"] + [instruction]
168
+ reasoning_response = llm.invoke(stack)
169
+ log_stage("REASONING", subtitle=f"{current_step_info.id}", icon="🧠")
170
+ print(reasoning_response.content)
171
 
172
+ return {
173
+ "messages": state["messages"] + [reasoning_response],
174
+ "reasoning_done": True,
175
+ }
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ available_tools = {tool.name for tool in TOOLS}
178
+ if current_step_info.tool and current_step_info.tool not in available_tools:
179
+ log_stage(
180
+ "TOOL WARNING",
181
+ subtitle=f"Unknown tool '{current_step_info.tool}' in plan",
182
+ icon="⚠️",
183
+ )
184
+ warning = AIMessage(
185
+ content=(
186
+ f"<REASONING>Unable to execute {current_step_info.id}: tool "
187
+ f"'{current_step_info.tool}' is unavailable. Requesting replanning.</REASONING>"
188
+ )
189
+ )
190
+ print(warning.content)
191
+ return {
192
+ "messages": state["messages"] + [warning],
193
+ "reasoning_done": False,
194
+ }
195
 
196
+ execution_instruction = HumanMessage(
197
+ content=(
198
+ "Execute the planned action now. If a tool is required, call it with the "
199
+ "correct arguments. After success, respond with STEP COMPLETE. If inputs are "
200
+ "missing, explain the issue in <REASONING> without new tool calls."
201
+ )
202
+ )
203
+ stack = [system_message] + state["messages"] + [execution_instruction]
204
+ execution_response = llm_with_tools.invoke(stack)
205
 
206
+ if execution_response.tool_calls:
207
+ tool_names = ", ".join(call["name"] for call in execution_response.tool_calls)
208
+ log_stage("TOOL CALL", subtitle=f"{current_step_info.id} → {tool_names}", icon="🛠️")
209
+ print(execution_response.tool_calls)
210
+ else:
211
+ log_stage("EXECUTION OUTPUT", subtitle=current_step_info.id, icon="🛠️")
212
+ if execution_response.content:
213
+ print(execution_response.content)
214
 
215
+ advance = False
216
+ if execution_response.tool_calls:
217
+ advance = True
218
+ elif execution_response.content and (
219
+ "STEP COMPLETE" in execution_response.content or "<FINAL_ANSWER>" in execution_response.content
220
+ ):
221
+ advance = True
222
+
223
+ next_step = current_step + 1 if advance and current_step < total_steps else current_step
224
+
225
+ return {
226
+ "messages": state["messages"] + [execution_response],
227
+ "current_step": next_step,
228
+ "reasoning_done": False,
229
+ }
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  def should_continue(state : AgentState) -> bool:
232
 
233
  last_message = state["messages"][-1]
 
257
  class DebuggingToolNode(ToolNode):
258
  def __init__(self, tools):
259
  super().__init__(tools)
260
+
261
  def __call__(self, state):
262
+ log_stage("TOOL NODE", subtitle="Dispatching tool calls", icon="🛠️")
263
+ try:
264
+ result = super().__call__(state)
265
+ log_stage("TOOL NODE", subtitle="Tool execution completed", icon="✅")
266
+ return result
267
+ except Exception as exc:
268
+ log_stage("TOOL ERROR", subtitle=f"{type(exc).__name__}: {exc}", icon="❌")
269
+ messages = state.get("messages", [])
270
+ last_message = messages[-1] if messages else None
271
+ tool_calls = getattr(last_message, "tool_calls", []) if last_message else []
272
+
273
+ error_messages = []
274
+ for call in tool_calls:
275
+ error_messages.append(
276
+ ToolMessage(
277
+ content=f"ERROR: {type(exc).__name__}: {exc}",
278
+ tool_call_id=call.get("id") or "unknown_call",
279
+ name=call.get("name"),
280
+ )
281
+ )
282
+
283
+ if not error_messages:
284
+ error_messages.append(
285
+ ToolMessage(
286
+ content=f"ERROR: {type(exc).__name__}: {exc}",
287
+ tool_call_id="unknown_call",
288
+ )
289
+ )
290
+
291
+ return {"messages": messages + error_messages}
292
 
293
 
294
 
295
  def enhanced_finalizer(state: AgentState) -> AgentState:
296
  """Generate comprehensive execution report for critic evaluation."""
297
+ log_stage("FINALIZER", subtitle="Compiling execution report", icon="📄")
298
+
299
  # Extract tool execution information
300
  tools_executed = []
301
  data_sources = []
 
320
  plan = state.get("plan")
321
  approach_used = "Direct execution"
322
  assumptions_made = []
323
+ plan_overview = ""
324
+
325
  if plan:
326
+ approach_used = f"{plan.task_type} plan {plan.summary}"
327
  assumptions_made = plan.assumptions
328
+ plan_overview = format_plan_overview(plan)
329
 
330
  # Generate structured report (КОСТЫЛЬ ЗДЕСЬ!)
331
  report_generator_prompt = f"""
332
  Generate a comprehensive execution report for the following query processing:
333
 
334
  ORIGINAL QUERY: {state['query']}
335
+
336
  EXECUTION CONTEXT:
337
  - Complexity Level: {state.get('complexity_assessment', {}).level}
338
+ - Plan Used: {plan_overview if plan_overview else 'direct response'}
339
  - Tools Executed: {tools_executed}
340
  - Available Files: {list(state.get('file_contents', {}).keys())}
341
 
 
362
  HumanMessage(content="Generate the execution report.")
363
  ])
364
 
365
+ log_key_values(
366
+ [
367
+ ("confidence", execution_report.confidence_level),
368
+ ("findings", str(len(execution_report.key_findings))),
369
+ ("sources", str(len(execution_report.data_sources))),
370
+ ]
371
+ )
372
+
373
  # Format final answer for user
374
  formatted_answer = format_final_answer(execution_report, state.get('complexity_assessment', {}))
375
+ log_stage("FINAL ANSWER PREVIEW", icon="📬")
376
+ print(formatted_answer)
377
  return {
378
  "execution_report": execution_report,
379
  "final_answer": formatted_answer
 
382
 
383
  def simple_executor(state: AgentState) -> AgentState:
384
  """Handle simple queries directly without planning."""
385
+ log_stage("SIMPLE EXECUTION", subtitle="Handling low-complexity query", icon="⚡")
386
+
387
  # For simple queries, use the LLM with tools directly
388
  simple_prompt = f"""
389
  Answer this simple query directly and efficiently: {state['query']}
390
+
391
+ Stay factual, cite tools only if you actually call them, and avoid inventing files or URLs.
392
+ Known files: {list(state.get('file_contents', {}).keys())}
393
+ If no tool is required, respond immediately with the final answer.
 
394
  """
395
+
396
  response = llm_with_tools.invoke([
397
  SystemMessage(content=simple_prompt),
398
  HumanMessage(content=state['query'])
399
  ])
400
 
401
+ log_stage("SIMPLE EXECUTION OUTPUT", icon="📬")
402
+ print(response.content)
403
+
404
  return {
405
  "messages": state["messages"] + [response],
406
  "final_answer": response.content
 
419
 
420
  def critic_evaluator(state: AgentState) -> AgentState:
421
  """Enhanced critic that evaluates execution reports."""
422
+ log_stage("CRITIC", subtitle="Evaluating execution report", icon="🔍")
423
+
424
  report = state.get("execution_report")
425
  critic_llm = llm.with_structured_output(CritiqueFeedback)
426
 
 
440
  HumanMessage(content="Evaluate this execution report thoroughly.")
441
  ])
442
 
443
+ log_key_values(
444
+ [
445
+ ("quality", f"{critique.quality_score}/10"),
446
+ ("complete", str(critique.is_complete)),
447
+ ("accurate", str(critique.is_accurate)),
448
+ ]
449
+ )
450
+
451
  if critique.errors_found:
452
+ log_stage("CRITIC ISSUES", icon="⚠️")
453
+ for issue in critique.errors_found:
454
+ print(f" - {issue}")
455
+
456
  if critique.needs_replanning:
457
+ log_stage("CRITIC REPLAN", subtitle="Replanning requested", icon="♻️")
458
+ print(critique.replan_instructions)
459
 
460
  return {
461
  "critique_feedback": critique,
 
469
  critique = state.get("critique_feedback")
470
  iteration_count = state.get("iteration_count", 0)
471
  max_iterations = state.get("max_iterations", 3)
 
472
 
473
+ subtitle = f"Iteration {iteration_count}/{max_iterations}"
474
+ log_stage("REPLAN DECISION", subtitle=subtitle, icon="🧭")
475
+ if critique:
476
+ log_key_values(
477
+ [
478
+ ("quality", str(critique.quality_score)),
479
+ ("needs_replanning", str(critique.needs_replanning)),
480
+ ]
481
+ )
482
 
483
  if not critique:
484
  return "end"
485
+
486
  # Stop if max iterations reached
487
  if iteration_count >= max_iterations:
488
+ log_stage("REPLAN DECISION", subtitle="Max iterations reached", icon="🛑")
489
  return "end"
490
+
491
  # Accept if quality is good enough
492
  if critique.quality_score >= 7 or not critique.needs_replanning:
493
+ log_stage("REPLAN DECISION", subtitle="Accepting current answer", icon="✅")
494
  return "end"
495
+
496
  # Replan if quality is poor and we haven't exceeded max iterations
497
  if critique.needs_replanning and iteration_count < max_iterations:
498
+ log_stage("REPLAN DECISION", subtitle="Triggering replanner", icon="♻️")
499
  return "replan"
500
+
501
  return "end"
502
 
503
  def replanner(state: AgentState) -> AgentState:
504
  """Create a revised plan based on critic feedback."""
505
+ log_stage("REPLANNER", subtitle="Adjusting plan based on feedback", icon="♻️")
506
+
507
  critique = state["critique_feedback"]
508
  previous_plan = state.get("plan")
509
+
510
+ previous_summary = previous_plan.summary if previous_plan else "no previous plan"
511
+ issues = ", ".join(critique.errors_found) if critique.errors_found else "none"
512
+ improvements = ", ".join(critique.suggested_improvements) if critique.suggested_improvements else "none"
513
+ extra_context = (
514
+ f"Replanning requested by critic. Previous plan summary: {previous_summary}. "
515
+ f"Critic score: {critique.quality_score}/10. Issues: {issues}. "
516
+ f"Improvements to address: {improvements}. Specific instructions: "
517
+ f"{critique.replan_instructions or 'none'}"
518
+ )
519
+
520
+ replan_prompt = _build_planner_prompt(state, extra_context=extra_context)
521
+
 
 
 
 
 
522
  revised_plan = planner_llm.invoke([
523
  SystemMessage(content=replan_prompt),
524
  HumanMessage(content="Create a revised plan based on the feedback.")
525
  ])
526
+
527
+ display_plan(revised_plan)
528
+
529
  # Очищаем историю сообщений от неполных tool_calls
530
  current_messages = state.get("messages", [])
531
  cleaned_messages = clean_message_history(current_messages)
 
540
  isinstance(msg, HumanMessage)):
541
  essential_messages.append(msg)
542
 
543
+ log_stage(
544
+ "REPLANNER",
545
+ subtitle=f"Cleaned history: {len(current_messages)} → {len(essential_messages)}",
546
+ icon="🧹",
547
+ )
548
+
549
  return {
550
  "plan": revised_plan,
551
  "current_step": 0,
 
557
 
558
  def complexity_assessor(state: AgentState) -> AgentState:
559
  """Assess query complexity and determine if planning is needed."""
560
+ log_stage("COMPLEXITY", subtitle="Assessing task difficulty", icon="📊")
561
+
562
  complexity_llm = llm.with_structured_output(ComplexityLevel)
563
+
564
  assessment_message = [
565
  SystemMessage(content=COMPLEXITY_ASSESSOR_PROMPT.strip()),
566
  HumanMessage(content=f"Query: {state['query']}")
567
  ]
568
+
569
  assessment = complexity_llm.invoke(assessment_message)
570
+ log_key_values(
571
+ [
572
+ ("level", assessment.level),
573
+ ("needs_planning", str(assessment.needs_planning)),
574
+ ("reasoning", assessment.reasoning),
575
+ ]
576
+ )
577
+
578
  return {
579
  "complexity_assessment": assessment,
580
  "messages": state["messages"] + assessment_message
src/prompts/prompts.py CHANGED
@@ -1,72 +1,56 @@
1
  SYSTEM_PROMPT_PLANNER = """
2
- You are the PLANNER of a multi-tool agent (GAIA I–II level). Produce a minimal, reliable plan to solve the user's request using available tools. You DO NOT call tools; output ONLY a JSON plan. Tools are bound via .bind_tools()—use EXACT names.
3
-
4
- CORE RULES:
5
- - MINIMALITY: 1-3 steps max; chain only essentials (e.g., search → download → analyze).
6
- - ROUTING: Classify as info (web facts), calc (math on known data), table (CSV/Excel agg), doc_qa (PDF/DOCX/TXT extract), image_qa (IMG OCR/vision), multi_hop (anything cross-modality or research—default for unknowns).
7
- - PREREQUISITES: For external docs/images (e.g., "paper X", URLs): ALWAYS start with web_search/arxiv_search → download_file_from_url (local path like "paper.pdf") → analyze_*. NEVER assume local files—validate existence implicitly via chain.
8
- - COST-AWARE: Cheap first: search snippets > full download > compute. No raw files to safe_code_run—extract first.
9
- - EVIDENCE: Mandate citations/pages for facts; units/rounding explicit in guidelines.
10
- - FALLBACKS: Every step needs success_criteria; on_fail="replan" (default) or "sN" (jump). Add 1 fallback step if high-risk (e.g., no-results → alt query).
11
-
12
- ROUTING PATTERNS (MANDATORY CHAINS):
13
- - info: web_search/wiki_search/arxiv_search → cite snippets.
14
- - calc: If data missing, insert extract step → safe_code_run (e.g., "sum volumes from text").
15
- - table: analyze_csv_file/analyze_excel_file (preview) → safe_code_run (agg/query).
16
- - doc_qa: web_search("paper title PDF") → download_file_from_url → analyze_pdf_file/analyze_docx_file (query="vials fluid ml") → safe_code_run if sum needed.
17
- - image_qa: web_search → download_file_from_url → analyze_image_file/vision_qa_gemma → safe_code_run for chart-to-table.
18
- - multi_hop: Decompose (e.g., sub-query1: search; sub-query2: extract) → synthesize.
19
-
20
- Output ONLY valid JSON:
21
- {
22
  "task_type": "info|calc|table|doc_qa|image_qa|multi_hop",
23
- "assumptions": ["..."], // 0-2 max; e.g., "Paper details vials explicitly"
24
- "plan_rationale": "Brief: why route + key tools/chain", // 1 sentence
25
- "steps": [ // 1-3 only
26
- {
27
  "id": "s1",
28
- "description": "Precise action + why (e.g., 'web_search for paper PDF to locate source')",
29
- "evidence_needed": ["citations","page_numbers","stats_check"], // 1-3
30
- "success_criteria": "e.g., 'Top result has PDF URL; or data extracted'",
31
- "on_fail": "replan|sN", // Default: replan
32
- "outputs_to_state": ["e.g., 'pdf_url', 'extracted_text'"] // For chaining
33
- }
34
  ],
35
- "answer_guidelines": {
36
- "final_answer_template": "e.g., 'Cumulative volume: X mL (from [cite])'",
37
- "citations_required": true,
38
- "min_citations": 1,
39
- "units_policy": "e.g., 'mL; convert if cm³'",
40
- "rounding_policy": "e.g., 'Nearest integer'",
41
- "include_artifacts": ["snippets","tables"] // 0-2
42
- }
43
- }
44
-
45
- CONSTRAINTS:
46
- - Valid JSON only—no extras. If query trivial (no tools), task_type="info" with 0 steps.
47
- - Exact tool names: web_search, download_file_from_url, analyze_pdf_file, safe_code_run, etc.
48
- - For research: If no chain, replan triggers auto-fix.
49
  """
50
 
51
  SYSTEM_EXECUTOR_PROMPT = """
52
- ROLE: EXECUTOR of multi-tool agent (GAIA level). You follow the FIXED {plan} EXACTLY—no changes, no new steps. Current step: {current_step_id} ("{step_desc}"). Advance ONE step per response.
53
-
54
- EXECUTION RULES:
55
- - BEFORE EVERY TOOL: <REASONING> (2-3 sentences: What step? Why this tool? Exact inputs? Expected output?) </REASONING>
56
- - THEN: Tool call ONLY for this step (exact name/args from plan). NO OTHER OUTPUT.
57
- - NO TOOLS? Direct output (e.g., "Calc: 5 mL") + set reasoning_done=True.
58
- - Check state for priors (e.g., if s2 needs pdf_url from s1, wait/replan if missing).
59
- - On fail (bad output): <REASONING>Assess + on_fail action</REASONING> then tool or stop.
60
- - END STEP: If success, output "STEP COMPLETE: {outputs_to_state}" to advance.
61
-
62
- RESOURCE CHAIN (MANDATORY IF NEEDED):
63
- - External doc? Use plan's search→download before analyze.
64
- - NEVER guess pathsuse state["files"] or replan.
65
-
66
- OUTPUT FORMAT: <REASONING>...</REASONING> [tool call or direct] [STEP COMPLETE if done]. NO JSON/PLANS/MARKDOWN.
67
-
68
- FAILSAFE: If unclear, <REASONING>Replan needed</REASONING> and stop.
69
- DO NOT FORGET TO ADD <FINAL_ANSWER> IF YOU THINK IT'S TIME TO ANSWER THE USER AND YOU HAVE ALL THE DATA FOR EXACT ANSWER.
70
  """
71
 
72
 
 
1
  SYSTEM_PROMPT_PLANNER = """
2
+ You are the planner of a multi-tool agent. Build a short, realistic plan that the executor can follow.
3
+
4
+ Available tools: {tool_catalogue}
5
+ Known local files: {file_list}
6
+ Additional context: {extra_context}
7
+
8
+ Return a single JSON object with this structure:
9
+ {{
 
 
 
 
 
 
 
 
 
 
 
 
10
  "task_type": "info|calc|table|doc_qa|image_qa|multi_hop",
11
+ "summary": "One sentence on the chosen approach",
12
+ "assumptions": ["optional clarifications"],
13
+ "steps": [
14
+ {{
15
  "id": "s1",
16
+ "goal": "Action to take and why it helps",
17
+ "tool": "tool_name_or_null",
18
+ "inputs": "Key parameters or references (files, URLs, prior steps)",
19
+ "expected_result": "How you know the step succeeded",
20
+ "on_fail": "replan|stop"
21
+ }}
22
  ],
23
+ "answer_guidelines": "Reminders for the final response (citations, format, units, etc.)"
24
+ }}
25
+
26
+ Ground rules:
27
+ - Prefer 1–3 steps. Only add a step if it changes the outcome.
28
+ - Use tool names exactly as listed. If no tool is needed, set "tool": null.
29
+ - Never assume files or URLs exist—plan to search/download before analysing.
30
+ - Skip download steps when the required file is already provided.
31
+ - Ensure later steps only depend on results created by earlier steps.
32
+ - If the query is trivial, return an empty steps list and explain the direct answer in "summary".
 
 
 
 
33
  """
34
 
35
  SYSTEM_EXECUTOR_PROMPT = """
36
+ You are the executor of a grounded multi-tool agent.
37
+
38
+ Plan summary: {plan_summary}
39
+ Step map:
40
+ {plan_overview}
41
+
42
+ Current focus: {current_step_id} {step_goal}
43
+ Suggested tool: {step_tool}
44
+ Available tools: {tool_catalogue}
45
+ Known local files: {file_list}
46
+
47
+ Execution rules:
48
+ 1. Stay aligned with the planno new steps or speculative actions.
49
+ 2. Before every tool call, respond with <REASONING>…</REASONING> explaining the step, chosen tool, inputs, and expected outcome.
50
+ 3. Call at most one tool per turn. After a successful step, state "STEP COMPLETE".
51
+ 4. If required inputs are missing (e.g., file not downloaded), explain the issue in <REASONING> and wait for replanning.
52
+ 5. Never invent file paths, URLs, or results. When unsure, request replanning instead of guessing.
53
+ 6. If no tool is needed, answer directly after the reasoning.
54
  """
55
 
56
 
src/schemas.py CHANGED
@@ -1,5 +1,5 @@
1
- from typing import Any, Dict, List, Optional, Literal, Iterable
2
- from pydantic import BaseModel, Field, ValidationError
3
 
4
 
5
  class ComplexityLevel(BaseModel):
@@ -21,32 +21,22 @@ class CritiqueFeedback(BaseModel):
21
 
22
 
23
  TaskType = Literal["info", "calc", "table", "doc_qa", "image_qa", "multi_hop"]
24
- EvidenceTag = Literal["citations", "page_numbers", "figure_captions", "stats_check", "unit_check"]
25
 
26
  class PlanStep(BaseModel):
27
- id: str
28
- description: str
29
- #tool: Optional[str] = Field(default=None, description="Exact tool name or null for reasoning step")
30
- #args_hint: Dict[str, Any] = Field(default_factory=dict)
31
- evidence_needed: List[EvidenceTag] = Field(default_factory=list)
32
- success_criteria: str
33
- on_fail: str = Field(default="replan", description="One of: 'replan' | 'stop' | step-id")
34
- outputs_to_state: List[str] = Field(default_factory=list)
35
 
36
- class AnswerGuidelines(BaseModel):
37
- final_answer_template: str
38
- citations_required: bool = False
39
- min_citations: int = 0
40
- units_policy: Optional[str] = None
41
- rounding_policy: Optional[str] = None
42
- include_artifacts: List[str] = Field(default_factory=list)
43
 
44
  class PlannerPlan(BaseModel):
45
  task_type: TaskType
 
46
  assumptions: List[str] = Field(default_factory=list)
47
- plan_rationale: str
48
- steps: List[PlanStep]
49
- answer_guidelines: AnswerGuidelines
50
 
51
 
52
  class ToolExecution(BaseModel):
 
1
+ from typing import List, Optional, Literal
2
+ from pydantic import BaseModel, Field
3
 
4
 
5
  class ComplexityLevel(BaseModel):
 
21
 
22
 
23
  TaskType = Literal["info", "calc", "table", "doc_qa", "image_qa", "multi_hop"]
 
24
 
25
  class PlanStep(BaseModel):
26
+ id: str = Field(description="Unique step identifier (e.g., s1)")
27
+ goal: str = Field(description="What the step accomplishes and why")
28
+ tool: Optional[str] = Field(default=None, description="Exact tool name or null when no tool is required")
29
+ inputs: Optional[str] = Field(default=None, description="Important inputs or references needed for the step")
30
+ expected_result: str = Field(description="How to confirm the step succeeded")
31
+ on_fail: str = Field(default="replan", description="Fallback action if the step fails (replan or stop)")
 
 
32
 
 
 
 
 
 
 
 
33
 
34
  class PlannerPlan(BaseModel):
35
  task_type: TaskType
36
+ summary: str = Field(description="Short explanation of the chosen strategy")
37
  assumptions: List[str] = Field(default_factory=list)
38
+ steps: List[PlanStep] = Field(default_factory=list)
39
+ answer_guidelines: Optional[str] = Field(default=None, description="Reminders for formatting, citations, etc.")
 
40
 
41
 
42
  class ToolExecution(BaseModel):
src/utils/utils.py CHANGED
@@ -1,9 +1,66 @@
 
 
1
  from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
2
- from schemas import ComplexityLevel, ExecutionReport
 
3
  from prompts.prompts import COMPLEXITY_ASSESSOR_PROMPT
4
  from config import llm
5
  from state import AgentState
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def clean_message_history(messages):
8
  """
9
  Очищает историю сообщений от неполных циклов tool_calls/responses.
 
1
+ from typing import Iterable, Optional
2
+
3
  from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
4
+
5
+ from schemas import ComplexityLevel, ExecutionReport, PlannerPlan
6
  from prompts.prompts import COMPLEXITY_ASSESSOR_PROMPT
7
  from config import llm
8
  from state import AgentState
9
 
10
+ def log_stage(title: str, subtitle: Optional[str] = None, icon: str = "🚀") -> None:
11
+ """Render a banner for the current execution stage."""
12
+
13
+ title_line = f" {title.strip()} "
14
+ border = icon + " " + "═" * max(len(title_line), 20)
15
+ print(f"\n{border}\n{icon} {title_line}\n{icon} " + "═" * max(len(title_line), 20))
16
+ if subtitle:
17
+ print(f"{icon} {subtitle}")
18
+
19
+
20
+ def log_key_values(pairs: Iterable[tuple[str, str]]) -> None:
21
+ """Pretty-print simple key/value diagnostics."""
22
+
23
+ for key, value in pairs:
24
+ print(f" • {key}: {value}")
25
+
26
+
27
+ def format_plan_overview(plan: PlannerPlan) -> str:
28
+ """Create a human-readable summary of plan steps."""
29
+
30
+ if not plan or not plan.steps:
31
+ return "(no steps – direct response)"
32
+
33
+ lines = []
34
+ for step in plan.steps:
35
+ tool_hint = step.tool if step.tool else "no tool"
36
+ lines.append(f"{step.id}: {step.goal} [{tool_hint}]")
37
+ return "\n".join(lines)
38
+
39
+
40
+ def display_plan(plan: PlannerPlan) -> None:
41
+ """Print plan contents in a compact, readable form."""
42
+
43
+ log_stage("PLANNER OUTPUT", icon="🧭")
44
+ print(f"Task type: {plan.task_type}")
45
+ print(f"Summary: {plan.summary}")
46
+ if plan.assumptions:
47
+ print("Assumptions:")
48
+ for item in plan.assumptions:
49
+ print(f" - {item}")
50
+ print("Steps:")
51
+ for step in plan.steps:
52
+ print(f" {step.id} → {step.goal}")
53
+ if step.tool:
54
+ print(f" tool: {step.tool}")
55
+ if step.inputs:
56
+ print(f" inputs: {step.inputs}")
57
+ print(f" expected: {step.expected_result}")
58
+ if step.on_fail:
59
+ print(f" on_fail: {step.on_fail}")
60
+ if plan.answer_guidelines:
61
+ print(f"Answer guidelines: {plan.answer_guidelines}")
62
+
63
+
64
  def clean_message_history(messages):
65
  """
66
  Очищает историю сообщений от неполных циклов tool_calls/responses.