KaiserShultz commited on
Commit
b40e9bd
Β·
1 Parent(s): f988705

Reworked system of routing should_continue, also logging for better managing and testing

Browse files
src/config.py CHANGED
@@ -3,6 +3,8 @@ from tools.tools import *
3
  from tools.code_interpreter import safe_code_run
4
  from langgraph.prebuilt import ToolNode
5
  from schemas import PlannerPlan
 
 
6
 
7
  config = {"configurable": {"thread_id": "1"}, "recursion_limit" : 50}
8
 
@@ -15,12 +17,71 @@ TOOLS = [download_file_from_url, web_search,
15
  class DebuggingToolNode(ToolNode):
16
  def __init__(self, tools):
17
  super().__init__(tools)
18
-
19
  def __call__(self, state):
20
- print("=== TOOL EXECUTION STARTED ===")
21
- result = super().__call__(state)
22
- print("=== TOOL EXECUTION COMPLETED ===")
23
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  TOOL_NODE = ToolNode(TOOLS)
 
3
  from tools.code_interpreter import safe_code_run
4
  from langgraph.prebuilt import ToolNode
5
  from schemas import PlannerPlan
6
+ from utils.utils import log_stage
7
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
8
 
9
  config = {"configurable": {"thread_id": "1"}, "recursion_limit" : 50}
10
 
 
17
  class DebuggingToolNode(ToolNode):
18
  def __init__(self, tools):
19
  super().__init__(tools)
20
+
21
  def __call__(self, state):
22
+ log_stage("TOOL NODE", subtitle="Dispatching tool calls", icon="πŸ› οΈ")
23
+
24
+ messages = state.get("messages", [])
25
+ last_message = messages[-1] if messages else None
26
+
27
+ if not last_message or not hasattr(last_message, "tool_calls"):
28
+ log_stage("TOOL ERROR", subtitle="No tool calls found", icon="❌")
29
+ return state
30
+
31
+ tool_calls = last_message.tool_calls
32
+ log_stage("TOOL DISPATCH", subtitle=f"Executing {len(tool_calls)} tool(s)", icon="πŸ”§")
33
+ for call in tool_calls:
34
+ print(f" - {call['name']}: {call['args']}")
35
+
36
+ try:
37
+ # ВыполняСм инструмСнты
38
+ result = super().__call__(state)
39
+
40
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Ρ‹
41
+ new_messages = result.get("messages", [])
42
+ tool_messages = [msg for msg in new_messages[len(messages):]
43
+ if isinstance(msg, ToolMessage)]
44
+
45
+ log_stage("TOOL RESULTS", subtitle=f"Got {len(tool_messages)} responses", icon="πŸ“¨")
46
+
47
+ # Π›ΠΎΠ³ΠΈΡ€ΡƒΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Ρ‹
48
+ for msg in tool_messages:
49
+ content_preview = msg.content[:100] + "..." if len(msg.content) > 100 else msg.content
50
+ print(f" - {msg.name}: {content_preview}")
51
+
52
+ # АвтоматичСски добавляСм сигнал Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ шага послС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ выполнСния инструмСнтов
53
+ if tool_messages:
54
+ current_step = state.get("current_step", 0)
55
+ plan = state.get("plan")
56
+
57
+ if plan and current_step < len(plan.steps):
58
+ step_completion_msg = AIMessage(
59
+ content=f"STEP COMPLETE: Successfully executed {len(tool_messages)} tool(s) for step {plan.steps[current_step].id}"
60
+ )
61
+ result["messages"] = result["messages"] + [step_completion_msg]
62
+ log_stage("STEP COMPLETION", subtitle=f"Step {current_step + 1} marked complete", icon="βœ…")
63
+
64
+ # ΠŸΡ€ΠΎΠ΄Π²ΠΈΠ³Π°Π΅ΠΌ ΠΊ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌΡƒ ΡˆΠ°Π³Ρƒ
65
+ result["current_step"] = current_step + 1
66
+ result["reasoning_done"] = False # Бброс для ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π³ΠΎ шага
67
+
68
+ return result
69
+
70
+ except Exception as exc:
71
+ log_stage("TOOL ERROR", subtitle=f"{type(exc).__name__}: {exc}", icon="❌")
72
+ print(f"Full error: {repr(exc)}")
73
+
74
+ # БоздаСм ToolMessage для каТдого failed tool call
75
+ error_messages = []
76
+ for call in tool_calls:
77
+ error_msg = ToolMessage(
78
+ content=f"ERROR: {type(exc).__name__}: {exc}",
79
+ tool_call_id=call.get("id") or "unknown_call",
80
+ name=call.get("name", "unknown_tool"),
81
+ )
82
+ error_messages.append(error_msg)
83
+
84
+ return {"messages": messages + error_messages}
85
 
86
 
87
  TOOL_NODE = ToolNode(TOOLS)
src/nodes.py CHANGED
@@ -1,9 +1,9 @@
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 (
@@ -12,8 +12,10 @@ from prompts.prompts import (
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,
@@ -23,7 +25,6 @@ from utils.utils import (
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", [])
@@ -35,15 +36,18 @@ def _build_planner_prompt(state: AgentState, extra_context: Optional[str] = None
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),
@@ -52,39 +56,42 @@ def query_input(state: AgentState) -> AgentState:
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,
@@ -95,19 +102,40 @@ def planner(state: AgentState) -> AgentState:
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:
@@ -127,6 +155,8 @@ def agent(state: AgentState) -> AgentState:
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}",
@@ -157,7 +187,9 @@ def agent(state: AgentState) -> AgentState:
157
  ).strip()
158
  )
159
 
 
160
  if not reasoning_done:
 
161
  instruction = HumanMessage(
162
  content=(
163
  "Provide reasoning for this step inside <REASONING>...</REASONING>. "
@@ -169,65 +201,67 @@ def agent(state: AgentState) -> AgentState:
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]
@@ -235,67 +269,48 @@ def should_continue(state : AgentState) -> bool:
235
  plan = state.get("plan", None)
236
  current_step = state.get("current_step", 0)
237
 
238
- if plan and current_step >= len(plan.steps):
239
- return "final_answer"
240
-
241
-
 
242
  if hasattr(last_message, "content") and "<FINAL_ANSWER>" in last_message.content:
243
  return "final_answer"
244
- elif hasattr(last_message, "tool_calls") and last_message.tool_calls:
245
- return "tools"
246
- elif not reasoning_done and hasattr(last_message, 'content') and "<REASONING>" in last_message.content:
247
  # Reasoning Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½, Π½ΠΎ инструмСнты Π΅Ρ‰Π΅ Π½Π΅ Π²Ρ‹Π·Π²Π°Π½Ρ‹
248
  return "agent"
249
  elif reasoning_done:
250
  # Reasoning Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½, Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π·Π²Π°Ρ‚ΡŒ инструмСнты
251
  return "agent"
252
- else:
253
  # НуТно ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ reasoning
254
  return "agent"
 
 
 
 
 
 
 
255
 
256
  # 6. Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΎΡ‚Π»Π°Π΄ΠΎΡ‡Π½ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ Π² TOOL_NODE
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,22 +335,20 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
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,18 +375,13 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
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,25 +390,23 @@ def enhanced_finalizer(state: AgentState) -> AgentState:
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,8 +425,8 @@ def should_use_planning(state: AgentState) -> str:
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,22 +446,15 @@ def critic_evaluator(state: AgentState) -> AgentState:
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,63 +468,64 @@ def should_replan(state: AgentState) -> str:
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,12 +540,8 @@ def replanner(state: AgentState) -> AgentState:
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,24 +553,21 @@ def replanner(state: AgentState) -> AgentState:
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
 
1
  import os
 
 
2
  from state import AgentState
3
  from tools.tools import preprocess_files
4
+ from typing import Optional
5
  from langgraph.prebuilt import ToolNode
6
+
7
  from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
8
 
9
  from prompts.prompts import (
 
12
  COMPLEXITY_ASSESSOR_PROMPT,
13
  CRITIC_PROMPT,
14
  )
15
+
16
  from config import llm, TOOLS, planner_llm, llm_with_tools
17
  from schemas import PlannerPlan, ComplexityLevel, CritiqueFeedback, ExecutionReport, ToolExecution
18
+
19
  from utils.utils import (
20
  format_final_answer,
21
  clean_message_history,
 
25
  format_plan_overview,
26
  )
27
 
 
28
  def _build_planner_prompt(state: AgentState, extra_context: Optional[str] = None) -> str:
29
  tool_catalogue = ", ".join(sorted(tool.name for tool in TOOLS))
30
  file_paths = state.get("files", [])
 
36
  extra_context=extra,
37
  ).strip()
38
 
39
+ def query_input(state : AgentState) -> AgentState:
40
  log_stage("USER QUERY", icon="πŸ’‘")
41
+ #print("=== USER QUERY TRANSFERED TO AGENT ===")
42
 
43
  files = state.get("files", [])
44
  if files:
45
+ print(f"Processing {len(files)} files:")
46
  log_stage("FILE PREPARATION", subtitle=f"Processing {len(files)} file(s)", icon="πŸ“")
47
  file_info = preprocess_files(files)
48
+
49
  for file_path, info in file_info.items():
50
+ print(f" - {file_path}: {info['type']} ({info['size']} bytes) -> {info['suggested_tool']}")
51
  log_key_values(
52
  [
53
  ("path", file_path),
 
56
  ("suggested_tool", info["suggested_tool"]),
57
  ]
58
  )
 
59
  state["file_contents"] = file_info
60
  file_context = "\n\n=== AVAILABLE FILES FOR ANALYSIS ===\n"
61
  for file_path, info in file_info.items():
62
  filename = os.path.basename(file_path)
63
  file_context += f"File: {filename}\n"
64
+ file_context += f" - Type: {info['type']}\n"
65
  file_context += f" - Size: {info['size']} bytes\n"
66
  file_context += f" - Suggested tool: {info['suggested_tool']}\n"
67
  if info.get("preview"):
68
  file_context += f" - Preview: {info['preview']}\n"
69
  file_context += "\n"
70
+
71
+ # ДобавляСм инструкции ΠΏΠΎ Ρ€Π°Π±ΠΎΡ‚Π΅ с Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ
72
  file_context += "IMPORTANT: Use the suggested tools to analyze these files before processing their data.\n"
73
  file_context += "File paths are available in the agent state and can be passed directly to analysis tools.\n"
74
+
 
 
75
  else:
76
  log_key_values([("files", "none provided")])
77
+ file_context = ""
78
+ original_query = state.get("query", "")
79
+ state["query"] = original_query + file_context
80
  return state
81
 
82
 
83
+ def planner(state : AgentState) -> AgentState:
84
+
85
  log_stage("PLANNING", icon="🧭")
86
  planner_prompt = _build_planner_prompt(state)
87
 
88
  sys_stack = [
89
+ SystemMessage(content=planner_prompt),
90
+ HumanMessage(content=state["query"]),
91
+ ]
92
  plan: PlannerPlan = planner_llm.invoke(sys_stack)
93
+
94
+ #print("=== GENERATED PLAN ===")
95
  display_plan(plan)
96
  return {
97
  "messages": state["messages"] + sys_stack,
 
102
 
103
 
104
  def agent(state: AgentState) -> AgentState:
105
+
106
+ """
107
+ sys_msg = SystemMessage(
108
+ content=SYSTEM_EXECUTOR_PROMPT.strip().format(
109
+ plan=json.dumps(state["plan"], indent=2)
110
+ )
111
+ )
112
+ """
113
  current_step = state.get("current_step", 0)
114
  reasoning_done = state.get("reasoning_done", False)
115
  plan: Optional[PlannerPlan] = state.get("plan")
116
+ #steps = state["plan"].steps
117
 
118
+ """
119
+ print(f"=== AGENT DEBUG ===")
120
+ print(f"Current step: {current_step}")
121
+ print(f"Reasoning done: {reasoning_done}")
122
+ print(f"Plan exists: {plan is not None}")
123
+ print(f"Total steps in plan: {len(plan.steps) if plan else 'No plan'}")
124
+
125
+ if not plan or not hasattr(plan, 'steps') or not plan.steps:
126
+ print("ERROR: No valid plan found!")
127
+ """
128
+
129
+ if not plan or not hasattr(plan, 'steps'):
130
  log_stage("PLAN VALIDATION", subtitle="Planner returned no actionable steps", icon="⚠️")
131
  warning = AIMessage(content="No valid plan available. <FINAL_ANSWER>")
132
  return {
133
  "messages": state["messages"] + [warning],
134
  "reasoning_done": False,
135
  }
136
+
137
  steps = plan.steps
138
+
139
  total_steps = len(steps)
140
 
141
  if total_steps == 0:
 
155
  }
156
 
157
  current_step_info = steps[current_step]
158
+ #print(f"Executing step {current_step + 1}: {current_step_info.description}")
159
+
160
  log_stage(
161
  "EXECUTION",
162
  subtitle=f"Step {current_step + 1}/{total_steps}: {current_step_info.goal}",
 
187
  ).strip()
188
  )
189
 
190
+
191
  if not reasoning_done:
192
+
193
  instruction = HumanMessage(
194
  content=(
195
  "Provide reasoning for this step inside <REASONING>...</REASONING>. "
 
201
  log_stage("REASONING", subtitle=f"{current_step_info.id}", icon="🧠")
202
  print(reasoning_response.content)
203
 
204
+ # βœ… Π”ΠžΠ‘ΠΠ’Π›Π•ΠΠž: Π‘ΠΏΠ΅Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΉ контСкст для Ρ„Π°ΠΉΠ»ΠΎΠ²
205
+ file_context = ""
206
+ file_contents = state.get("file_contents", {})
207
+ if file_contents:
208
+ file_context = "\n\nAVAILABLE FILES IN CURRENT SESSION:\n"
209
+ for filepath, info in file_contents.items():
210
+ filename = os.path.basename(filepath)
211
+ file_context += f"- {filename}: {info['type']} file, suggested tool: {info['suggested_tool']}\n"
212
+ file_context += f" Path: {filepath}\n"
213
+
214
+ reasoning_prompt = f"""
215
+ {SYSTEM_EXECUTOR_PROMPT}
216
+
217
+ CURRENT TASK: You must perform reasoning for step {current_step + 1}.
218
+
219
+ STEP INFO: {current_step_info}\n\n
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
+ FILE CONTEXT: {file_contents}
222
+
223
+ CRITICAL: You MUST output your reasoning in <REASONING> tags, but DO NOT call any tools yet.
224
+ Explain what you need to do and why, then end your response.
 
 
 
 
225
 
226
+ REASONING IS IMPERATIVE BEFORE ANY TOOL CALLS.
227
+ """
 
 
 
 
 
228
 
229
+ sys_msg = SystemMessage(content = reasoning_prompt)
230
+ stack = [sys_msg] + state["messages"]
231
 
232
+ step = llm.invoke(stack)
233
+ print("=== REASONING STEP ===")
234
+ print(step.content)
 
 
235
 
236
+ return {
237
+ "messages" : state["messages"] + [step],
238
+ "reasoning_done" : True
239
+ }
240
+
241
+ else:
242
+ tool_prompt = f"""
243
+ Now execute the tool for step {current_step + 1}.
244
+
245
+ You have already done the reasoning. Now call the appropriate tool with the correct parameters.
246
+ Available file paths: {list(state.get("file_contents", {}).keys())}\n
247
+ 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!
248
+ AVAILABLE TOOLS: {', '.join([tool.name for tool in TOOLS])}
249
+ """
250
+
251
+ sys_msg = SystemMessage(content=tool_prompt)
252
+ stack = [sys_msg] + state["messages"] # Π‘Π΅Ρ€Π΅ΠΌ послСдниС сообщСния Π²ΠΊΠ»ΡŽΡ‡Π°Ρ reasoning
253
+
254
+ # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ модСль Π‘ инструмСнтами для выполнСния
255
+ step = llm_with_tools.invoke(stack)
256
+ print("=== TOOL EXECUTION ===")
257
+ print(f"Tool calls: {step.tool_calls}")
258
+
259
+ return {
260
+ "messages": state["messages"] + [step],
261
+ "current_step": current_step + 1 if step.tool_calls else current_step,
262
+ "reasoning_done": False # БбрасываСм для ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π³ΠΎ шага
263
+ }
264
+
265
  def should_continue(state : AgentState) -> bool:
266
 
267
  last_message = state["messages"][-1]
 
269
  plan = state.get("plan", None)
270
  current_step = state.get("current_step", 0)
271
 
272
+ #ΠŸΠ Π˜ΠžΠ Π˜Π’Π•Π’ 1: Если Π΅ΡΡ‚ΡŒ tool_calls - выполняСм ΠΈΡ…
273
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
274
+ return "tools"
275
+
276
+ # ΠŸΠ Π˜ΠžΠ Π˜Π’Π•Π’ 2: Π―Π²Π½Ρ‹ΠΉ сигнал Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ
277
  if hasattr(last_message, "content") and "<FINAL_ANSWER>" in last_message.content:
278
  return "final_answer"
279
+
280
+ # ΠŸΠ Π˜ΠžΠ Π˜Π’Π•Π’ 3: Π›ΠΎΠ³ΠΈΠΊΠ° reasoning/execution
281
+ if not reasoning_done and hasattr(last_message, 'content') and "<REASONING>" in last_message.content:
282
  # Reasoning Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½, Π½ΠΎ инструмСнты Π΅Ρ‰Π΅ Π½Π΅ Π²Ρ‹Π·Π²Π°Π½Ρ‹
283
  return "agent"
284
  elif reasoning_done:
285
  # Reasoning Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½, Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π·Π²Π°Ρ‚ΡŒ инструмСнты
286
  return "agent"
287
+ elif not reasoning_done:
288
  # НуТно ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ reasoning
289
  return "agent"
290
+
291
+ # ΠŸΠ Π˜ΠžΠ Π˜Π’Π•Π’ 4: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΠ΅ ΠΏΠ»Π°Π½Π° (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ссли Π½Π΅Ρ‚ Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… tool_calls)
292
+ if plan and current_step >= len(plan.steps):
293
+ return "final_answer"
294
+
295
+ # По ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅
296
+ return "agent"
297
 
298
  # 6. Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΎΡ‚Π»Π°Π΄ΠΎΡ‡Π½ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ Π² TOOL_NODE
299
  class DebuggingToolNode(ToolNode):
300
  def __init__(self, tools):
301
  super().__init__(tools)
302
+
303
  def __call__(self, state):
304
+ print("=== TOOL EXECUTION STARTED ===")
305
+ result = super().__call__(state)
306
+ print("=== TOOL EXECUTION COMPLETED ===")
307
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
 
 
310
  def enhanced_finalizer(state: AgentState) -> AgentState:
311
  """Generate comprehensive execution report for critic evaluation."""
312
+ print("=== GENERATING EXECUTION REPORT ===")
313
+
314
  # Extract tool execution information
315
  tools_executed = []
316
  data_sources = []
 
335
  plan = state.get("plan")
336
  approach_used = "Direct execution"
337
  assumptions_made = []
338
+
 
339
  if plan:
340
+ approach_used = f"{plan.task_type} approach with {len(plan.steps)} steps"
341
  assumptions_made = plan.assumptions
 
342
 
343
  # Generate structured report (ΠšΠžΠ‘Π’Π«Π›Π¬ Π—Π”Π•Π‘Π¬!)
344
  report_generator_prompt = f"""
345
  Generate a comprehensive execution report for the following query processing:
346
 
347
  ORIGINAL QUERY: {state['query']}
348
+
349
  EXECUTION CONTEXT:
350
  - Complexity Level: {state.get('complexity_assessment', {}).level}
351
+ - Plan Used: {plan if plan else {}}
352
  - Tools Executed: {tools_executed}
353
  - Available Files: {list(state.get('file_contents', {}).keys())}
354
 
 
375
  HumanMessage(content="Generate the execution report.")
376
  ])
377
 
378
+ print(f"Report generated - Confidence: {execution_report.confidence_level}")
379
+ print(f"Key findings: {len(execution_report.key_findings)}")
380
+ print(f"Data sources: {len(execution_report.data_sources)}")
381
+
 
 
 
 
382
  # Format final answer for user
383
  formatted_answer = format_final_answer(execution_report, state.get('complexity_assessment', {}))
384
+ print(execution_report)
 
385
  return {
386
  "execution_report": execution_report,
387
  "final_answer": formatted_answer
 
390
 
391
  def simple_executor(state: AgentState) -> AgentState:
392
  """Handle simple queries directly without planning."""
393
+ print("=== SIMPLE EXECUTION ===")
394
+
395
  # For simple queries, use the LLM with tools directly
396
  simple_prompt = f"""
397
  Answer this simple query directly and efficiently: {state['query']}
398
+
399
+ You have access to tools if needed, but try to answer directly when possible.
400
+ If you need files, they are available at: {list(state.get('file_contents', {}).keys())}
401
+
402
+ Provide a clear, concise answer.
403
  """
404
+
405
  response = llm_with_tools.invoke([
406
  SystemMessage(content=simple_prompt),
407
  HumanMessage(content=state['query'])
408
  ])
409
 
 
 
 
410
  return {
411
  "messages": state["messages"] + [response],
412
  "final_answer": response.content
 
425
 
426
  def critic_evaluator(state: AgentState) -> AgentState:
427
  """Enhanced critic that evaluates execution reports."""
428
+ print("=== ENHANCED ANSWER CRITIQUE ===")
429
+
430
  report = state.get("execution_report")
431
  critic_llm = llm.with_structured_output(CritiqueFeedback)
432
 
 
446
  HumanMessage(content="Evaluate this execution report thoroughly.")
447
  ])
448
 
449
+ print(f"Quality Score: {critique.quality_score}/10")
450
+ print(f"Complete: {critique.is_complete}")
451
+ print(f"Accurate: {critique.is_accurate}")
452
+
 
 
 
 
453
  if critique.errors_found:
454
+ print(f"Issues found: {critique.errors_found}")
455
+
 
 
456
  if critique.needs_replanning:
457
+ print(f"Replanning needed: {critique.replan_instructions}")
 
458
 
459
  return {
460
  "critique_feedback": critique,
 
468
  critique = state.get("critique_feedback")
469
  iteration_count = state.get("iteration_count", 0)
470
  max_iterations = state.get("max_iterations", 3)
471
+
472
 
473
+ print(f"=== REPLAN DECISION ===")
474
+ print(f"Iteration: {iteration_count}/{max_iterations}")
475
+ print(f"Quality score: {critique.quality_score if critique else 'N/A'}")
476
+ print(f"Needs replanning: {critique.needs_replanning if critique else 'N/A'}")
 
 
 
 
 
477
 
478
  if not critique:
479
  return "end"
480
+
481
  # Stop if max iterations reached
482
  if iteration_count >= max_iterations:
483
+ print(f"Max iterations ({max_iterations}) reached. Accepting current answer.")
484
  return "end"
485
+
486
  # Accept if quality is good enough
487
  if critique.quality_score >= 7 or not critique.needs_replanning:
488
+ print("Quality acceptable, ending execution")
489
  return "end"
490
+
491
  # Replan if quality is poor and we haven't exceeded max iterations
492
  if critique.needs_replanning and iteration_count < max_iterations:
493
+ print("Replanning due to critic feedback...")
494
  return "replan"
495
+
496
  return "end"
497
 
498
  def replanner(state: AgentState) -> AgentState:
499
  """Create a revised plan based on critic feedback."""
500
+ print("=== REPLANNING ===")
501
+
502
  critique = state["critique_feedback"]
503
  previous_plan = state.get("plan")
504
+
505
+ replan_prompt = f"""
506
+ {SYSTEM_PROMPT_PLANNER}
507
+
508
+ REPLANNING CONTEXT:
509
+ Original Query: {state['query']}
510
+ Previous Plan: {previous_plan if previous_plan else {}}
511
+
512
+ CRITIC FEEDBACK:
513
+ - Quality Score: {critique.quality_score}/10
514
+ - Issues Found: {critique.errors_found}
515
+ - Missing Elements: {critique.missing_elements}
516
+ - Improvement Suggestions: {critique.suggested_improvements}
517
+ - Specific Instructions: {critique.replan_instructions}
518
+
519
+ Create a REVISED plan that addresses these issues. Focus on fixing the identified problems.
520
+ """
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
+ print("Plan revised based on critic feedback")
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
+ print(f"Cleaned message history: {len(current_messages)} -> {len(essential_messages)} messages")
544
+
 
 
 
 
545
  return {
546
  "plan": revised_plan,
547
  "current_step": 0,
 
553
 
554
  def complexity_assessor(state: AgentState) -> AgentState:
555
  """Assess query complexity and determine if planning is needed."""
556
+ print("=== COMPLEXITY ASSESSMENT ===")
557
+
558
  complexity_llm = llm.with_structured_output(ComplexityLevel)
559
+
560
  assessment_message = [
561
  SystemMessage(content=COMPLEXITY_ASSESSOR_PROMPT.strip()),
562
  HumanMessage(content=f"Query: {state['query']}")
563
  ]
564
+
565
  assessment = complexity_llm.invoke(assessment_message)
566
+
567
+ print(f"Complexity: {assessment.level}")
568
+ print(f"Needs planning: {assessment.needs_planning}")
569
+ print(f"Reasoning: {assessment.reasoning}")
570
+
 
 
 
571
  return {
572
  "complexity_assessment": assessment,
573
  "messages": state["messages"] + assessment_message
src/prompts/prompts.py CHANGED
@@ -24,7 +24,7 @@ Return a single JSON object with this structure:
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.
@@ -130,5 +130,5 @@ Confidence: {confidence}
130
  Limitations: {limitations}
131
  Final Answer: {answer}
132
 
133
- Provide detailed critique focusing on what works well and what could be improved.
134
  """
 
24
  }}
25
 
26
  Ground rules:
27
+ - Prefer 1–3 steps. Only add a step if it changes the outcome. For complex tasks, up to 5-7 steps is okay.
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.
 
130
  Limitations: {limitations}
131
  Final Answer: {answer}
132
 
133
+ Provide detailed critique focusing on what works well and what could be improved. REMEMBER: if the task is enough simple, just say "NO CRITIC NEEDED".
134
  """
src/schemas.py CHANGED
@@ -1,6 +1,5 @@
1
  from typing import List, Optional, Literal
2
- from pydantic import BaseModel, Field
3
-
4
 
5
  class ComplexityLevel(BaseModel):
6
  level: Literal["simple", "moderate", "complex"] = Field(description="Complexity level of the query")
@@ -18,8 +17,6 @@ class CritiqueFeedback(BaseModel):
18
  needs_replanning: bool = Field(description="Whether the plan should be revised")
19
  replan_instructions: Optional[str] = Field(default=None, description="Instructions for replanning")
20
 
21
-
22
-
23
  TaskType = Literal["info", "calc", "table", "doc_qa", "image_qa", "multi_hop"]
24
 
25
  class PlanStep(BaseModel):
@@ -30,6 +27,19 @@ class PlanStep(BaseModel):
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
 
1
  from typing import List, Optional, Literal
2
+ from pydantic import BaseModel, Field, field_validator
 
3
 
4
  class ComplexityLevel(BaseModel):
5
  level: Literal["simple", "moderate", "complex"] = Field(description="Complexity level of the query")
 
17
  needs_replanning: bool = Field(description="Whether the plan should be revised")
18
  replan_instructions: Optional[str] = Field(default=None, description="Instructions for replanning")
19
 
 
 
20
  TaskType = Literal["info", "calc", "table", "doc_qa", "image_qa", "multi_hop"]
21
 
22
  class PlanStep(BaseModel):
 
27
  expected_result: str = Field(description="How to confirm the step succeeded")
28
  on_fail: str = Field(default="replan", description="Fallback action if the step fails (replan or stop)")
29
 
30
+ @field_validator("tool", mode="before")
31
+ @classmethod
32
+ def normalize_tool(cls, value: Optional[str]) -> Optional[str]:
33
+ """Ensure blank or null-like values are interpreted as no tool."""
34
+
35
+ if value is None:
36
+ return None
37
+ if isinstance(value, str):
38
+ cleaned = value.strip()
39
+ if not cleaned or cleaned.lower() in {"null", "none"}:
40
+ return None
41
+ return cleaned
42
+ return value
43
 
44
  class PlannerPlan(BaseModel):
45
  task_type: TaskType
src/utils/utils.py CHANGED
@@ -1,10 +1,9 @@
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:
@@ -52,6 +51,8 @@ def display_plan(plan: PlannerPlan) -> None:
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}")
@@ -138,7 +139,7 @@ def complexity_assessor(state: AgentState) -> AgentState:
138
  """Assess query complexity and determine if planning is needed."""
139
  print("=== COMPLEXITY ASSESSMENT ===")
140
 
141
- complexity_llm = llm.with_structured_output(ComplexityLevel)
142
 
143
  assessment_message = [
144
  SystemMessage(content=COMPLEXITY_ASSESSOR_PROMPT.strip()),
 
1
  from typing import Iterable, Optional
2
+ from langchain_openai import ChatOpenAI
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 state import AgentState
8
 
9
  def log_stage(title: str, subtitle: Optional[str] = None, icon: str = "πŸš€") -> None:
 
51
  print(f" {step.id} β†’ {step.goal}")
52
  if step.tool:
53
  print(f" tool: {step.tool}")
54
+ else:
55
+ print(" tool: (none)")
56
  if step.inputs:
57
  print(f" inputs: {step.inputs}")
58
  print(f" expected: {step.expected_result}")
 
139
  """Assess query complexity and determine if planning is needed."""
140
  print("=== COMPLEXITY ASSESSMENT ===")
141
 
142
+ complexity_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.25).with_structured_output(ComplexityLevel)
143
 
144
  assessment_message = [
145
  SystemMessage(content=COMPLEXITY_ASSESSOR_PROMPT.strip()),
src/workflow_test.ipynb CHANGED
@@ -2,14 +2,14 @@
2
  "cells": [
3
  {
4
  "cell_type": "code",
5
- "execution_count": 2,
6
  "metadata": {},
7
  "outputs": [
8
  {
9
  "name": "stderr",
10
  "output_type": "stream",
11
  "text": [
12
- "d:\\REGNUM_SPECTRARUM_updated\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
13
  " from .autonotebook import tqdm as notebook_tqdm\n"
14
  ]
15
  }
@@ -21,7 +21,7 @@
21
  },
22
  {
23
  "cell_type": "code",
24
- "execution_count": 3,
25
  "metadata": {},
26
  "outputs": [],
27
  "source": [
@@ -30,46 +30,188 @@
30
  },
31
  {
32
  "cell_type": "code",
33
- "execution_count": 4,
34
  "metadata": {},
35
  "outputs": [
36
  {
37
  "name": "stdout",
38
  "output_type": "stream",
39
  "text": [
40
- "=== USER QUERY TRANSFERED TO AGENT ===\n",
 
 
 
 
41
  "=== COMPLEXITY ASSESSMENT ===\n",
42
- "Complexity: simple\n",
43
- "Needs planning: False\n",
44
- "Reasoning: This query is a straightforward arithmetic calculation that can be answered immediately without any tools or complex reasoning.\n",
45
- "=== SIMPLE EXECUTION ===\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  "=== GENERATING EXECUTION REPORT ===\n",
47
  "Report generated - Confidence: high\n",
48
  "Key findings: 2\n",
49
- "Data sources: 0\n",
50
- "query_summary='The user requested the result of the arithmetic expression 2 + 2 - 2 + 2.' approach_used='The query was evaluated using basic arithmetic operations, following the standard order of operations.' tools_executed=[] key_findings=['The expression was simplified step-by-step: 2 + 2 = 4, then 4 - 2 = 2, and finally 2 + 2 = 4.', 'The final result of the expression is 4.'] data_sources=[] assumptions_made=['The user intended to use standard arithmetic rules without any additional context or modifications.'] confidence_level='high' limitations=['The query was straightforward, and no complex tools or external data sources were required.', 'The execution context did not involve any ambiguity or alternative interpretations.'] final_answer='4'\n",
51
  "=== ENHANCED ANSWER CRITIQUE ===\n",
52
- "Quality Score: 8/10\n",
53
  "Complete: True\n",
54
  "Accurate: True\n",
55
  "=== REPLAN DECISION ===\n",
56
  "Iteration: 1/10\n",
57
- "Quality score: 8\n",
58
  "Needs replanning: False\n",
59
  "Quality acceptable, ending execution\n"
60
  ]
61
  }
62
  ],
63
  "source": [
64
- "result = graph.invoke({\"query\" : \"2+2-2+2?\", \"current_step\": 0, \"reasoning_done\": False, \"files\" : [], \"files_contents\" : {}, \"iteration_count\" : 0, \"max_iterations\" : 10, \"plan\" : None} , config = config)"
 
65
  ]
66
  },
67
  {
68
  "cell_type": "code",
69
- "execution_count": null,
70
  "metadata": {},
71
- "outputs": [],
72
- "source": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
  ],
75
  "metadata": {
 
2
  "cells": [
3
  {
4
  "cell_type": "code",
5
+ "execution_count": 1,
6
  "metadata": {},
7
  "outputs": [
8
  {
9
  "name": "stderr",
10
  "output_type": "stream",
11
  "text": [
12
+ "d:\\ankelodon_multiagent_system\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
13
  " from .autonotebook import tqdm as notebook_tqdm\n"
14
  ]
15
  }
 
21
  },
22
  {
23
  "cell_type": "code",
24
+ "execution_count": 2,
25
  "metadata": {},
26
  "outputs": [],
27
  "source": [
 
30
  },
31
  {
32
  "cell_type": "code",
33
+ "execution_count": 3,
34
  "metadata": {},
35
  "outputs": [
36
  {
37
  "name": "stdout",
38
  "output_type": "stream",
39
  "text": [
40
+ "\n",
41
+ "πŸ’‘ ════════════════════\n",
42
+ "πŸ’‘ USER QUERY \n",
43
+ "πŸ’‘ ════════════════════\n",
44
+ " β€’ files: none provided\n",
45
  "=== COMPLEXITY ASSESSMENT ===\n",
46
+ "Complexity: complex\n",
47
+ "Needs planning: True\n",
48
+ "Reasoning: This query involves multiple steps: first, gathering information about Nikita Miroshnichenko, which may require searching through various sources; second, verifying his affiliation with UNIL and any working experience at EPFL; and third, synthesizing this information into a coherent summary. The need to cross-reference information adds to the complexity, as it requires careful reasoning to ensure accuracy.\n",
49
+ "\n",
50
+ "🧭 ════════════════════\n",
51
+ "🧭 PLANNING \n",
52
+ "🧭 ════════════════════\n",
53
+ "\n",
54
+ "🧭 ════════════════════\n",
55
+ "🧭 PLANNER OUTPUT \n",
56
+ "🧭 ════════════════════\n",
57
+ "Task type: info\n",
58
+ "Summary: I will perform a web search to gather information about Nikita Miroshnichenko, including his background as a student at UNIL and any working experience at EPFL.\n",
59
+ "Steps:\n",
60
+ " s1 β†’ Search for information about Nikita Miroshnichenko to confirm his background and work experience.\n",
61
+ " tool: web_search\n",
62
+ " inputs: Nikita Miroshnichenko UNIL EPFL\n",
63
+ " expected: Find relevant information confirming his student status and any work experience at EPFL.\n",
64
+ " on_fail: replan\n",
65
+ "Answer guidelines: Provide a concise summary based on the information found, including citations if applicable.\n",
66
+ "\n",
67
+ "πŸ€– ════════════════════\n",
68
+ "πŸ€– EXECUTION \n",
69
+ "πŸ€– ════════════════════\n",
70
+ "πŸ€– Step 1/1: Search for information about Nikita Miroshnichenko to confirm his background and work experience.\n",
71
+ " β€’ step_id: s1\n",
72
+ " β€’ tool: web_search\n",
73
+ " β€’ expected: Find relevant information confirming his student status and any work experience at EPFL.\n",
74
+ "\n",
75
+ "🧠 ════════════════════\n",
76
+ "🧠 REASONING \n",
77
+ "🧠 ════════════════════\n",
78
+ "🧠 s1\n",
79
+ "<REASONING> The query requires gathering information about Nikita Miroshnichenko, specifically his background as a student at UNIL and any work experience at EPFL. This involves performing a web search to find relevant details about him, which will help in writing a short summary. The first step will be to use the web_search tool to collect this information. The expected outcome is to obtain sufficient data to confirm his educational background and work experience, which will then allow for the creation of a summary. Since this is a research task that requires external information, it is classified as a moderate complexity task. </REASONING>\n",
80
+ "=== REASONING STEP ===\n",
81
+ "{\n",
82
+ " \"task_type\": \"info\",\n",
83
+ " \"summary\": \"The plan involves searching for information about Nikita Miroshnichenko to confirm his background as a student at UNIL and any work experience at EPFL.\",\n",
84
+ " \"assumptions\": [\"Nikita Miroshnichenko is a student at UNIL\", \"There may be publicly available information regarding his work experience at EPFL\"],\n",
85
+ " \"steps\": [\n",
86
+ " {\n",
87
+ " \"id\": \"s1\",\n",
88
+ " \"goal\": \"Search for information about Nikita Miroshnichenko to confirm his background and work experience.\",\n",
89
+ " \"tool\": \"web_search\",\n",
90
+ " \"inputs\": \"Nikita Miroshnichenko UNIL EPFL\",\n",
91
+ " \"expected_result\": \"Find relevant information confirming his student status and any work experience at EPFL.\",\n",
92
+ " \"on_fail\": \"replan\"\n",
93
+ " }\n",
94
+ " ],\n",
95
+ " \"answer_guidelines\": \"Provide a summary of the findings, including citations for any sources used.\"\n",
96
+ "}\n",
97
+ "\n",
98
+ "πŸ€– ════════════════════\n",
99
+ "πŸ€– EXECUTION \n",
100
+ "πŸ€– ════════════════════\n",
101
+ "πŸ€– Step 1/1: Search for information about Nikita Miroshnichenko to confirm his background and work experience.\n",
102
+ " β€’ step_id: s1\n",
103
+ " β€’ tool: web_search\n",
104
+ " β€’ expected: Find relevant information confirming his student status and any work experience at EPFL.\n",
105
+ "=== TOOL EXECUTION ===\n",
106
+ "Tool calls: [{'name': 'web_search', 'args': {'query': 'Nikita Miroshnichenko UNIL EPFL'}, 'id': 'call_TJN5zTZWXac12m0so0FrKpOr', 'type': 'tool_call'}]\n"
107
+ ]
108
+ },
109
+ {
110
+ "name": "stderr",
111
+ "output_type": "stream",
112
+ "text": [
113
+ "d:\\ankelodon_multiagent_system\\src\\tools\\tools.py:228: LangChainDeprecationWarning: The class `TavilySearchResults` was deprecated in LangChain 0.3.25 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-tavily package and should be used instead. To use it run `pip install -U :class:`~langchain-tavily` and import as `from :class:`~langchain_tavily import TavilySearch``.\n",
114
+ " raw_results = TavilySearchResults(max_results=max_results).invoke(query)\n"
115
+ ]
116
+ },
117
+ {
118
+ "name": "stdout",
119
+ "output_type": "stream",
120
+ "text": [
121
+ "\n",
122
+ "βœ… ════════════════════\n",
123
+ "βœ… PLAN COMPLETE \n",
124
+ "βœ… ════════════════════\n",
125
+ "βœ… All steps executed\n",
126
  "=== GENERATING EXECUTION REPORT ===\n",
127
  "Report generated - Confidence: high\n",
128
  "Key findings: 2\n",
129
+ "Data sources: 1\n",
130
+ "query_summary='The user requested information about Nikita Miroshnichenko, a student from UNIL, and inquired about his working experience at EPFL.' approach_used=\"A web search was conducted to gather relevant information regarding Nikita Miroshnichenko's background as a student at UNIL and any work experience he may have at EPFL.\" tools_executed=[ToolExecution(tool_name='web_search', arguments=\"{'query': 'Nikita Miroshnichenko UNIL EPFL'}\", call_id='call_TJN5zTZWXac12m0so0FrKpOr')] key_findings=['Nikita Miroshnichenko is a student at UNIL.', 'He has been associated with EPFL, confirming his work experience there.'] data_sources=['https://topline.com/people/nikita-miroshnichenko-182776498'] assumptions_made=[] confidence_level='high' limitations=['The information retrieved is based on available online sources, which may not be exhaustive or fully up-to-date.'] final_answer='Nikita Miroshnichenko is a student at UNIL and has confirmed working experience at EPFL.'\n",
131
  "=== ENHANCED ANSWER CRITIQUE ===\n",
132
+ "Quality Score: 6/10\n",
133
  "Complete: True\n",
134
  "Accurate: True\n",
135
  "=== REPLAN DECISION ===\n",
136
  "Iteration: 1/10\n",
137
+ "Quality score: 6\n",
138
  "Needs replanning: False\n",
139
  "Quality acceptable, ending execution\n"
140
  ]
141
  }
142
  ],
143
  "source": [
144
+ "query = \"Find info about Nikita Miroshnichenko, its a student from UNIL, and write a short summary about him. Is it true that he has a working experience at EPFL?\"\n",
145
+ "result = graph.invoke({\"query\" : query, \"current_step\": 0, \"reasoning_done\": False, \"files\" : [], \"files_contents\" : {}, \"iteration_count\" : 0, \"max_iterations\" : 10, \"plan\" : None} , config = config)"
146
  ]
147
  },
148
  {
149
  "cell_type": "code",
150
+ "execution_count": 7,
151
  "metadata": {},
152
+ "outputs": [
153
+ {
154
+ "name": "stdout",
155
+ "output_type": "stream",
156
+ "text": [
157
+ "FINAL ANSWER: Nikita Miroshnichenko is a student at UNIL and has confirmed working experience at EPFL.\n",
158
+ "\n",
159
+ "SUMMARY:\n",
160
+ "The user requested information about Nikita Miroshnichenko, a student from UNIL, and inquired about his working experience at EPFL.\n",
161
+ "\n",
162
+ "KEY FINDINGS:\n",
163
+ "β€’ Nikita Miroshnichenko is a student at UNIL.\n",
164
+ "β€’ He has been associated with EPFL, confirming his work experience there.\n",
165
+ "\n",
166
+ "SOURCES:\n",
167
+ "β€’ https://topline.com/people/nikita-miroshnichenko-182776498\n",
168
+ "\n",
169
+ "LIMITATIONS:\n",
170
+ "β€’ The information retrieved is based on available online sources, which may not be exhaustive or fully up-to-date.\n"
171
+ ]
172
+ }
173
+ ],
174
+ "source": [
175
+ "print(result[\"final_answer\"])"
176
+ ]
177
+ },
178
+ {
179
+ "cell_type": "code",
180
+ "execution_count": 8,
181
+ "metadata": {},
182
+ "outputs": [
183
+ {
184
+ "data": {
185
+ "text/plain": [
186
+ "{'messages': [SystemMessage(content='You are a COMPLEXITY ASSESSOR for a multi-tool agent system.\\nYour job is to analyze user queries and determine their complexity level and processing requirements.\\n\\nCOMPLEXITY LEVELS:\\n1. SIMPLE: Direct questions that can be answered immediately without tools or with single tool use\\n - Examples: \"What is 2+2?\", \"Define photosynthesis\", \"What\\'s the capital of France?\"\\n \\n2. MODERATE: Questions requiring 1-3 tool calls or basic analysis\\n - Examples: \"Search for recent news about AI\", \"Analyze this CSV file\", \"What\\'s the weather tomorrow?\"\\n \\n3. COMPLEX: Multi-step problems requiring planning, multiple tools, or sophisticated reasoning\\n - Examples: Research tasks, multi-file analysis, calculations with dependencies, creative projects\\n\\nASSESSMENT CRITERIA:\\n- Number of steps likely needed\\n- Tool complexity and dependencies\\n- Data processing requirements\\n- Need for intermediate reasoning\\n- Risk of failure without proper planning\\n\\nRULES:\\n- SIMPLE queries bypass planning entirely\\n- MODERATE queries may use lightweight planning\\n- COMPLEX queries require full planning with fallbacks\\n- When in doubt, err toward higher complexity\\n\\nAnalyze the query and respond with your assessment.', additional_kwargs={}, response_metadata={}, id='11b7e36b-63f4-4dab-b911-19a122ded253'),\n",
187
+ " HumanMessage(content='Query: Find info about Nikita Miroshnichenko, its a student from UNIL, and write a short summary about him. Is it true that he has a working experience at EPFL?', additional_kwargs={}, response_metadata={}, id='33ed5c6b-c4af-4080-8531-afa04709ae79'),\n",
188
+ " SystemMessage(content='You are the planner of a multi-tool agent. Build a short, realistic plan that the executor can follow.\\n\\nAvailable tools: add, analyze_csv_file, analyze_docx_file, analyze_excel_file, analyze_image_file, analyze_pdf_file, analyze_txt_file, arxiv_search, divide, download_file_from_url, multiply, power, safe_code_run, subtract, vision_qa_gemma, web_search, wiki_search\\nKnown local files: none provided\\nAdditional context: None\\n\\nReturn a single JSON object with this structure:\\n{\\n \"task_type\": \"info|calc|table|doc_qa|image_qa|multi_hop\",\\n \"summary\": \"One sentence on the chosen approach\",\\n \"assumptions\": [\"optional clarifications\"],\\n \"steps\": [\\n {\\n \"id\": \"s1\",\\n \"goal\": \"Action to take and why it helps\",\\n \"tool\": \"tool_name_or_null\",\\n \"inputs\": \"Key parameters or references (files, URLs, prior steps)\",\\n \"expected_result\": \"How you know the step succeeded\",\\n \"on_fail\": \"replan|stop\"\\n }\\n ],\\n \"answer_guidelines\": \"Reminders for the final response (citations, format, units, etc.)\"\\n}\\n\\nGround rules:\\n- Prefer 1–3 steps. Only add a step if it changes the outcome. For complex tasks, up to 5-7 steps is okay.\\n- Use tool names exactly as listed. If no tool is needed, set \"tool\": null.\\n- Never assume files or URLs existβ€”plan to search/download before analysing.\\n- Skip download steps when the required file is already provided.\\n- Ensure later steps only depend on results created by earlier steps.\\n- If the query is trivial, return an empty steps list and explain the direct answer in \"summary\".', additional_kwargs={}, response_metadata={}, id='a2291408-86bd-4a5a-ad97-88ba7ca26f8a'),\n",
189
+ " HumanMessage(content='Find info about Nikita Miroshnichenko, its a student from UNIL, and write a short summary about him. Is it true that he has a working experience at EPFL?', additional_kwargs={}, response_metadata={}, id='02bdaf14-e770-4903-8bd2-ec4bce7070a0'),\n",
190
+ " AIMessage(content='{\\n \"task_type\": \"info\",\\n \"summary\": \"The plan involves searching for information about Nikita Miroshnichenko to confirm his background as a student at UNIL and any work experience at EPFL.\",\\n \"assumptions\": [\"Nikita Miroshnichenko is a student at UNIL\", \"There may be publicly available information regarding his work experience at EPFL\"],\\n \"steps\": [\\n {\\n \"id\": \"s1\",\\n \"goal\": \"Search for information about Nikita Miroshnichenko to confirm his background and work experience.\",\\n \"tool\": \"web_search\",\\n \"inputs\": \"Nikita Miroshnichenko UNIL EPFL\",\\n \"expected_result\": \"Find relevant information confirming his student status and any work experience at EPFL.\",\\n \"on_fail\": \"replan\"\\n }\\n ],\\n \"answer_guidelines\": \"Provide a summary of the findings, including citations for any sources used.\"\\n}', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 206, 'prompt_tokens': 1088, 'total_tokens': 1294, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_51db84afab', 'id': 'chatcmpl-CHEdmytbl8Nei62qo9Ti4se6AOc5O', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--bbddcd4a-f737-4033-b789-adcf6c3bacb5-0', usage_metadata={'input_tokens': 1088, 'output_tokens': 206, 'total_tokens': 1294, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),\n",
191
+ " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TJN5zTZWXac12m0so0FrKpOr', 'function': {'arguments': '{\"query\":\"Nikita Miroshnichenko UNIL EPFL\"}', 'name': 'web_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 25, 'prompt_tokens': 2672, 'total_tokens': 2697, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 1920}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_560af6e559', 'id': 'chatcmpl-CHEdrHAP7W9cDsebqQu7iAIVGl2OF', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--9d9e841f-b6eb-4df1-8695-3cb5d35e83b7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'Nikita Miroshnichenko UNIL EPFL'}, 'id': 'call_TJN5zTZWXac12m0so0FrKpOr', 'type': 'tool_call'}], usage_metadata={'input_tokens': 2672, 'output_tokens': 25, 'total_tokens': 2697, 'input_token_details': {'audio': 0, 'cache_read': 1920}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),\n",
192
+ " ToolMessage(content='{\"query\": \"Nikita Miroshnichenko UNIL EPFL\", \"provider\": \"tavily\", \"items\": [{\"url\": \"https://topline.com/people/nikita-miroshnichenko-182776498\", \"title\": \"Nikita Miroshnichenko - Topline\", \"snippet\": \"###### The International Festival of Engineering Science and Technology in Tunisia I-FEST\\\\u00b2 (Silver medal)\\\\n\\\\n###### Molecular phylogenetic&bioinformatics course\\\\n\\\\n###### Molecular biology&genetics course\\\\n\\\\n###### Python programming course\\\\n\\\\n#### Experience\\\\n\\\\n##### University of Lausanne - UNIL\\\\n\\\\n##### Student Laboratory Assistant\\\\n\\\\n##### EPFL (\\\\u00c9cole polytechnique f\\\\u00e9d\\\\u00e9rale de Lausanne)\\\\n\\\\n##### Laboratory A\\\\u2026\", \"published\": null, \"source\": \"topline.com\"}, {\"url\": \"https://cdn5.f-cdn.com/files/download/223843566/Nikita_Miroshnichenko_technical_CV_2024.pdf\", \"title\": \"[PDF] Nikita Miroshnichenko\", \"snippet\": \"development in this field. If possible, I will be glad to apply my work experience in biotech/neurotech-oriented projects. NOVEMBER 2023 \\\\u2013 PRESENT Research Assistant. Computational Biology and Cancer Genomics Group Department of Computational Biology | UNIL. Lausanne \\\\u2022 Processing raw single-cell RNAseq data, building bioinformatics pipelines for oncology and genomic research. \\\\u2022 Development of a P\\\\u2026\", \"published\": null, \"source\": \"f-cdn.com\"}, {\"url\": \"https://ch.linkedin.com/in/nikita-miroshnichenko\", \"title\": \"Nikita Miroshnichenko \\\\u2013 AI Engineer | Biotech Enthusiast - LinkedIn\", \"snippet\": \"Nikita Miroshnichenko. AI Engineer | Biotech Enthusiast | Researcher | Entrepreneur. University of Lausanne - UNIL Taras Shevchenko National University of Kyiv\", \"published\": null, \"source\": \"linkedin.com\"}, {\"url\": \"https://www.transfermarkt.com/nikita-miroshnichenko/profil/spieler/561855\", \"title\": \"Nikita Miroshnichenko - Player profile 25/26 - Transfermarkt\", \"snippet\": \"Transfermarkt\\\\nUEFA Champions League\\\\nPremier League\\\\nLaLiga\\\\nSerie A\\\\nBundesliga\\\\nLigue 1\\\\n\\\\nNikita Miroshnichenko\\\\n\\\\n# #18 Nikita Miroshnichenko\\\\n\\\\nShinnik Yaroslavl\\\\n1.Division1.Division\\\\nRussiaSecond Tier\\\\nNikita Miroshnichenko\\\\n\\\\nfnl.pro\\\\n\\\\n+\\\\nRussia \\\\nSalavat, ... \\\\nRussia Russia\\\\nRussiaRussia U17\\\\n\\\\nLast update: 02.06.2025\\\\n\\\\n## Player data\\\\n\\\\nRussia\\\\nRussia Russia\\\\nShinnik Yaroslavl\\\\n\\\\n## Stats of Nikita Miroshnichenko\\\\n\\\\u2026\", \"published\": null, \"source\": \"transfermarkt.com\"}, {\"url\": \"http://arxiv.org/list/physics.optics/2019-12?skip=125&show=2000\", \"title\": \"Optics Dec 2019 - arXiv\", \"snippet\": \"Cornell University\\\\narxiv logo\\\\n\\\\nHelp | Advanced Search\\\\n\\\\narXiv logo\\\\nCornell University Logo\\\\n\\\\n## quick links\\\\n\\\\n# Optics\\\\n\\\\n## Authors and titles for December 2019\\\\n\\\\narXiv Operational Status \\\\nGet status notifications via\\\\nemail\\\\nor slack\", \"published\": null, \"source\": \"arxiv.org\"}]}', name='web_search', id='cd84b5bb-27ed-495b-942a-70de16013c44', tool_call_id='call_TJN5zTZWXac12m0so0FrKpOr'),\n",
193
+ " AIMessage(content='All plan steps completed. <FINAL_ANSWER>', additional_kwargs={}, response_metadata={}, id='3463b080-aa10-4906-bc0c-4d6e49bb8d8a')],\n",
194
+ " 'query': 'Find info about Nikita Miroshnichenko, its a student from UNIL, and write a short summary about him. Is it true that he has a working experience at EPFL?',\n",
195
+ " 'final_answer': 'FINAL ANSWER: Nikita Miroshnichenko is a student at UNIL and has confirmed working experience at EPFL.\\n\\nSUMMARY:\\nThe user requested information about Nikita Miroshnichenko, a student from UNIL, and inquired about his working experience at EPFL.\\n\\nKEY FINDINGS:\\nβ€’ Nikita Miroshnichenko is a student at UNIL.\\nβ€’ He has been associated with EPFL, confirming his work experience there.\\n\\nSOURCES:\\nβ€’ https://topline.com/people/nikita-miroshnichenko-182776498\\n\\nLIMITATIONS:\\nβ€’ The information retrieved is based on available online sources, which may not be exhaustive or fully up-to-date.',\n",
196
+ " 'plan': PlannerPlan(task_type='info', summary='I will perform a web search to gather information about Nikita Miroshnichenko, including his background as a student at UNIL and any working experience at EPFL.', assumptions=[], steps=[PlanStep(id='s1', goal='Search for information about Nikita Miroshnichenko to confirm his background and work experience.', tool='web_search', inputs='Nikita Miroshnichenko UNIL EPFL', expected_result='Find relevant information confirming his student status and any work experience at EPFL.', on_fail='replan')], answer_guidelines='Provide a concise summary based on the information found, including citations if applicable.'),\n",
197
+ " 'complexity_assessment': ComplexityLevel(level='complex', reasoning='This query involves multiple steps: first, gathering information about Nikita Miroshnichenko, which may require searching through various sources; second, verifying his affiliation with UNIL and any working experience at EPFL; and third, synthesizing this information into a coherent summary. The need to cross-reference information adds to the complexity, as it requires careful reasoning to ensure accuracy.', needs_planning=True, suggested_approach='Begin by searching for Nikita Miroshnichenko on academic and professional platforms to gather relevant information. Verify his student status at UNIL and check for any records of employment or internships at EPFL. Compile the findings into a concise summary.'),\n",
198
+ " 'current_step': 1,\n",
199
+ " 'reasoning_done': False,\n",
200
+ " 'files': [],\n",
201
+ " 'critique_feedback': CritiqueFeedback(quality_score=6, is_complete=True, is_accurate=True, missing_elements=['Details about the specific role or position held by Nikita Miroshnichenko at EPFL', 'Information on the duration of his work experience at EPFL', 'Any notable projects or contributions made during his time at EPFL'], errors_found=[], suggested_improvements=[\"Include more specific details about Nikita's role at EPFL to provide a clearer picture of his experience.\", 'Add information about the duration of his work experience to contextualize his involvement.', 'Mention any projects or contributions he made during his time at EPFL to enhance the depth of the report.'], needs_replanning=False, replan_instructions=None),\n",
202
+ " 'iteration_count': 1,\n",
203
+ " 'max_iterations': 10,\n",
204
+ " 'execution_report': ExecutionReport(query_summary='The user requested information about Nikita Miroshnichenko, a student from UNIL, and inquired about his working experience at EPFL.', approach_used=\"A web search was conducted to gather relevant information regarding Nikita Miroshnichenko's background as a student at UNIL and any work experience he may have at EPFL.\", tools_executed=[ToolExecution(tool_name='web_search', arguments=\"{'query': 'Nikita Miroshnichenko UNIL EPFL'}\", call_id='call_TJN5zTZWXac12m0so0FrKpOr')], key_findings=['Nikita Miroshnichenko is a student at UNIL.', 'He has been associated with EPFL, confirming his work experience there.'], data_sources=['https://topline.com/people/nikita-miroshnichenko-182776498'], assumptions_made=[], confidence_level='high', limitations=['The information retrieved is based on available online sources, which may not be exhaustive or fully up-to-date.'], final_answer='Nikita Miroshnichenko is a student at UNIL and has confirmed working experience at EPFL.')}"
205
+ ]
206
+ },
207
+ "execution_count": 8,
208
+ "metadata": {},
209
+ "output_type": "execute_result"
210
+ }
211
+ ],
212
+ "source": [
213
+ "result"
214
+ ]
215
  }
216
  ],
217
  "metadata": {