aiecs 1.7.6__py3-none-any.whl → 1.7.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiecs might be problematic. Click here for more details.
- aiecs/__init__.py +1 -1
- aiecs/config/tool_config.py +55 -19
- aiecs/domain/agent/base_agent.py +79 -0
- aiecs/domain/agent/hybrid_agent.py +468 -172
- aiecs/domain/agent/models.py +10 -0
- aiecs/domain/agent/tools/schema_generator.py +17 -4
- aiecs/llm/client_factory.py +6 -1
- aiecs/llm/clients/base_client.py +5 -1
- aiecs/llm/clients/google_function_calling_mixin.py +46 -88
- aiecs/llm/clients/googleai_client.py +79 -6
- aiecs/llm/clients/vertex_client.py +310 -21
- aiecs/main.py +2 -2
- aiecs/tools/docs/document_creator_tool.py +143 -2
- aiecs/tools/docs/document_parser_tool.py +9 -4
- aiecs/tools/docs/document_writer_tool.py +179 -0
- aiecs/tools/task_tools/image_tool.py +49 -14
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/METADATA +1 -1
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/RECORD +22 -22
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/WHEEL +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/entry_points.txt +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.7.17.dist-info}/top_level.txt +0 -0
|
@@ -43,6 +43,67 @@ class HybridAgent(BaseAIAgent):
|
|
|
43
43
|
- BaseLLMClient: Standard LLM clients (OpenAI, xAI, etc.)
|
|
44
44
|
- Custom clients: Any object implementing LLMClientProtocol (duck typing)
|
|
45
45
|
|
|
46
|
+
**ReAct Format Reference (for callers to include in their prompts):**
|
|
47
|
+
|
|
48
|
+
The caller is responsible for ensuring the LLM follows the correct format.
|
|
49
|
+
Below are the standard formats that HybridAgent expects:
|
|
50
|
+
|
|
51
|
+
CORRECT FORMAT EXAMPLE::
|
|
52
|
+
|
|
53
|
+
<THOUGHT>
|
|
54
|
+
I need to search for information about the weather. Let me use the search tool.
|
|
55
|
+
</THOUGHT>
|
|
56
|
+
|
|
57
|
+
TOOL: search
|
|
58
|
+
OPERATION: query
|
|
59
|
+
PARAMETERS: {"q": "weather today"}
|
|
60
|
+
|
|
61
|
+
<OBSERVATION>
|
|
62
|
+
The search tool returned: Today's weather is sunny, 72°F.
|
|
63
|
+
</OBSERVATION>
|
|
64
|
+
|
|
65
|
+
<THOUGHT>
|
|
66
|
+
I have the weather information. Now I can provide the final response.
|
|
67
|
+
</THOUGHT>
|
|
68
|
+
|
|
69
|
+
FINAL RESPONSE: Today's weather is sunny, 72°F. finish
|
|
70
|
+
|
|
71
|
+
INCORRECT FORMAT (DO NOT DO THIS)::
|
|
72
|
+
|
|
73
|
+
<THOUGHT>
|
|
74
|
+
I need to search.
|
|
75
|
+
TOOL: search
|
|
76
|
+
OPERATION: query
|
|
77
|
+
</THOUGHT>
|
|
78
|
+
❌ Tool calls must be OUTSIDE the <THOUGHT> and <OBSERVATION> tags
|
|
79
|
+
|
|
80
|
+
<THOUGHT>
|
|
81
|
+
I know the answer.
|
|
82
|
+
FINAL RESPONSE: The answer is... finish
|
|
83
|
+
</THOUGHT>
|
|
84
|
+
❌ Final responses must be OUTSIDE the <THOUGHT> and <OBSERVATION> tags
|
|
85
|
+
❌ FINAL RESPONSE must end with 'finish' suffix to indicate completion
|
|
86
|
+
|
|
87
|
+
TOOL CALL FORMAT::
|
|
88
|
+
|
|
89
|
+
TOOL: <tool_name>
|
|
90
|
+
OPERATION: <operation_name>
|
|
91
|
+
PARAMETERS: <json_parameters>
|
|
92
|
+
|
|
93
|
+
FINAL RESPONSE FORMAT::
|
|
94
|
+
|
|
95
|
+
FINAL RESPONSE: <your_response> finish
|
|
96
|
+
|
|
97
|
+
**Important Notes for Callers:**
|
|
98
|
+
|
|
99
|
+
- FINAL RESPONSE MUST end with 'finish' to indicate completion
|
|
100
|
+
- If no 'finish' suffix, the system assumes response is incomplete and will continue iteration
|
|
101
|
+
- LLM can output JSON or any text format - it will be passed through unchanged
|
|
102
|
+
- Each iteration will inform LLM of current iteration number and remaining iterations
|
|
103
|
+
- If LLM generation is incomplete, it will be asked to continue from where it left off
|
|
104
|
+
- Callers can customize max_iterations to control loop behavior
|
|
105
|
+
- Callers are responsible for parsing and handling LLM output format
|
|
106
|
+
|
|
46
107
|
Examples:
|
|
47
108
|
# Example 1: Basic usage with tool names (backward compatible)
|
|
48
109
|
agent = HybridAgent(
|
|
@@ -339,17 +400,29 @@ class HybridAgent(BaseAIAgent):
|
|
|
339
400
|
|
|
340
401
|
# Add ReAct instructions (always required for HybridAgent)
|
|
341
402
|
parts.append(
|
|
342
|
-
"
|
|
343
|
-
"Follow the ReAct pattern:\n"
|
|
403
|
+
"Within the given identity framework, you are also a highly intelligent, responsive, and accurate reasoning agent. that can use tools to complete tasks. "
|
|
404
|
+
"Follow the ReAct (Reasoning + Acting) pattern to achieve best results:\n"
|
|
344
405
|
"1. THOUGHT: Analyze the task and decide what to do\n"
|
|
345
406
|
"2. ACTION: Use a tool if needed, or provide final answer\n"
|
|
346
407
|
"3. OBSERVATION: Review the tool result and continue reasoning\n\n"
|
|
347
|
-
"
|
|
348
|
-
"
|
|
349
|
-
"
|
|
350
|
-
"PARAMETERS: <
|
|
351
|
-
"
|
|
352
|
-
"
|
|
408
|
+
"RESPONSE FORMAT REQUIREMENTS:\n"
|
|
409
|
+
"- Wrap your thinking process in <THOUGHT>...</THOUGHT> tags\n"
|
|
410
|
+
"- Wrap your insight about tool result in <OBSERVATION>...</OBSERVATION> tags\n"
|
|
411
|
+
"- Tool calls (TOOL:, OPERATION:, PARAMETERS:) MUST be OUTSIDE <THOUGHT> and <OBSERVATION> tags\n"
|
|
412
|
+
"- Final responses (FINAL RESPONSE:) MUST be OUTSIDE <THOUGHT> and <OBSERVATION> tags\n\n"
|
|
413
|
+
"THINKING GUIDANCE:\n"
|
|
414
|
+
"When writing <THOUGHT> sections, consider:\n"
|
|
415
|
+
"- What is the core thing to do?\n"
|
|
416
|
+
"- What information do I already have?\n"
|
|
417
|
+
"- What information do I need to gather?\n"
|
|
418
|
+
"- Which tools would be most helpful?\n"
|
|
419
|
+
"- What action should I take?\n\n"
|
|
420
|
+
"OBSERVATION GUIDANCE:\n"
|
|
421
|
+
"When writing <OBSERVATION> sections, consider:\n"
|
|
422
|
+
"- What did I learn from the tool results?\n"
|
|
423
|
+
"- How does this information inform my next work?\n"
|
|
424
|
+
"- Do I need additional information?\n"
|
|
425
|
+
"- Am I ready to provide a final response?"
|
|
353
426
|
)
|
|
354
427
|
|
|
355
428
|
# Add available tools (always required for HybridAgent)
|
|
@@ -408,7 +481,7 @@ class HybridAgent(BaseAIAgent):
|
|
|
408
481
|
|
|
409
482
|
return {
|
|
410
483
|
"success": True,
|
|
411
|
-
"output": result.get("
|
|
484
|
+
"output": result.get("final_response"), # Changed from final_answer
|
|
412
485
|
"reasoning_steps": result.get("steps"),
|
|
413
486
|
"tool_calls_count": result.get("tool_calls_count"),
|
|
414
487
|
"iterations": result.get("iterations"),
|
|
@@ -605,11 +678,23 @@ class HybridAgent(BaseAIAgent):
|
|
|
605
678
|
for iteration in range(self._max_iterations):
|
|
606
679
|
logger.debug(f"HybridAgent {self.agent_id} - ReAct iteration {iteration + 1}")
|
|
607
680
|
|
|
681
|
+
# Add iteration info to messages (except first iteration which has task context)
|
|
682
|
+
if iteration > 0:
|
|
683
|
+
iteration_info = (
|
|
684
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
685
|
+
f"remaining: {self._max_iterations - iteration - 1}]"
|
|
686
|
+
)
|
|
687
|
+
# Only add if the last message is not already an iteration info
|
|
688
|
+
if messages and not messages[-1].content.startswith("[Iteration"):
|
|
689
|
+
messages.append(LLMMessage(role="user", content=iteration_info))
|
|
690
|
+
|
|
608
691
|
# Yield iteration status
|
|
609
692
|
yield {
|
|
610
693
|
"type": "status",
|
|
611
694
|
"status": "thinking",
|
|
612
695
|
"iteration": iteration + 1,
|
|
696
|
+
"max_iterations": self._max_iterations,
|
|
697
|
+
"remaining": self._max_iterations - iteration - 1,
|
|
613
698
|
"timestamp": datetime.utcnow().isoformat(),
|
|
614
699
|
}
|
|
615
700
|
|
|
@@ -677,7 +762,16 @@ class HybridAgent(BaseAIAgent):
|
|
|
677
762
|
"timestamp": datetime.utcnow().isoformat(),
|
|
678
763
|
}
|
|
679
764
|
|
|
680
|
-
|
|
765
|
+
thought_raw = "".join(thought_tokens)
|
|
766
|
+
|
|
767
|
+
# Store raw output in steps (no format processing)
|
|
768
|
+
steps.append(
|
|
769
|
+
{
|
|
770
|
+
"type": "thought",
|
|
771
|
+
"content": thought_raw.strip(), # Return raw output without processing
|
|
772
|
+
"iteration": iteration + 1,
|
|
773
|
+
}
|
|
774
|
+
)
|
|
681
775
|
|
|
682
776
|
# Process tool_calls if received from stream
|
|
683
777
|
if tool_calls_from_stream:
|
|
@@ -688,19 +782,30 @@ class HybridAgent(BaseAIAgent):
|
|
|
688
782
|
func_args = tool_call["function"]["arguments"]
|
|
689
783
|
|
|
690
784
|
# Parse function name to extract tool and operation
|
|
691
|
-
|
|
692
|
-
if
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
785
|
+
# CRITICAL: Try exact match first, then fall back to underscore parsing
|
|
786
|
+
if self._tool_instances and func_name in self._tool_instances:
|
|
787
|
+
# Exact match found - use full function name as tool name
|
|
788
|
+
tool_name = func_name
|
|
789
|
+
operation = None
|
|
790
|
+
elif self._available_tools and func_name in self._available_tools:
|
|
791
|
+
# Exact match in available tools list
|
|
792
|
+
tool_name = func_name
|
|
696
793
|
operation = None
|
|
794
|
+
else:
|
|
795
|
+
# Fallback: try underscore parsing for legacy compatibility
|
|
796
|
+
parts = func_name.split("_", 1)
|
|
797
|
+
if len(parts) == 2:
|
|
798
|
+
tool_name, operation = parts
|
|
799
|
+
else:
|
|
800
|
+
tool_name = parts[0]
|
|
801
|
+
operation = None
|
|
697
802
|
|
|
698
803
|
# Parse arguments JSON
|
|
699
804
|
import json
|
|
700
805
|
if isinstance(func_args, str):
|
|
701
806
|
parameters = json.loads(func_args)
|
|
702
807
|
else:
|
|
703
|
-
parameters = func_args
|
|
808
|
+
parameters = func_args if func_args else {}
|
|
704
809
|
|
|
705
810
|
# Yield tool call event
|
|
706
811
|
yield {
|
|
@@ -715,17 +820,19 @@ class HybridAgent(BaseAIAgent):
|
|
|
715
820
|
tool_result = await self._execute_tool(tool_name, operation, parameters)
|
|
716
821
|
tool_calls_count += 1
|
|
717
822
|
|
|
823
|
+
# Wrap tool call and result in step
|
|
718
824
|
steps.append(
|
|
719
825
|
{
|
|
720
826
|
"type": "action",
|
|
721
827
|
"tool": tool_name,
|
|
722
828
|
"operation": operation,
|
|
723
829
|
"parameters": parameters,
|
|
830
|
+
"result": str(tool_result), # Include result in step
|
|
724
831
|
"iteration": iteration + 1,
|
|
725
832
|
}
|
|
726
833
|
)
|
|
727
834
|
|
|
728
|
-
# Yield tool result event
|
|
835
|
+
# Yield tool result event (streaming)
|
|
729
836
|
yield {
|
|
730
837
|
"type": "tool_result",
|
|
731
838
|
"tool_name": tool_name,
|
|
@@ -733,15 +840,9 @@ class HybridAgent(BaseAIAgent):
|
|
|
733
840
|
"timestamp": datetime.utcnow().isoformat(),
|
|
734
841
|
}
|
|
735
842
|
|
|
736
|
-
# Add tool result to messages
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
{
|
|
740
|
-
"type": "observation",
|
|
741
|
-
"content": observation,
|
|
742
|
-
"iteration": iteration + 1,
|
|
743
|
-
}
|
|
744
|
-
)
|
|
843
|
+
# Add tool result to messages (for LLM consumption)
|
|
844
|
+
observation_content = f"Tool '{tool_name}' returned: {tool_result}"
|
|
845
|
+
observation = f"<OBSERVATION>\n{observation_content}\n</OBSERVATION>"
|
|
745
846
|
|
|
746
847
|
# Add assistant message with tool call and tool result
|
|
747
848
|
messages.append(
|
|
@@ -760,13 +861,14 @@ class HybridAgent(BaseAIAgent):
|
|
|
760
861
|
)
|
|
761
862
|
|
|
762
863
|
except Exception as e:
|
|
763
|
-
|
|
864
|
+
error_content = f"Tool execution failed: {str(e)}"
|
|
865
|
+
error_msg = f"<OBSERVATION>\n{error_content}\n</OBSERVATION>"
|
|
764
866
|
steps.append(
|
|
765
867
|
{
|
|
766
868
|
"type": "observation",
|
|
767
869
|
"content": error_msg,
|
|
768
870
|
"iteration": iteration + 1,
|
|
769
|
-
"
|
|
871
|
+
"has_error": True,
|
|
770
872
|
}
|
|
771
873
|
)
|
|
772
874
|
yield {
|
|
@@ -786,21 +888,13 @@ class HybridAgent(BaseAIAgent):
|
|
|
786
888
|
# Continue to next iteration
|
|
787
889
|
continue
|
|
788
890
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
"content": thought,
|
|
793
|
-
"iteration": iteration + 1,
|
|
794
|
-
}
|
|
795
|
-
)
|
|
796
|
-
|
|
797
|
-
# Check if final answer
|
|
798
|
-
if "FINAL ANSWER:" in thought:
|
|
799
|
-
final_answer = self._extract_final_answer(thought)
|
|
891
|
+
# Check for final response (outside tags only)
|
|
892
|
+
if self._has_final_response(thought_raw):
|
|
893
|
+
final_response = self._extract_final_response(thought_raw)
|
|
800
894
|
yield {
|
|
801
895
|
"type": "result",
|
|
802
896
|
"success": True,
|
|
803
|
-
"output":
|
|
897
|
+
"output": final_response, # Return raw output without processing
|
|
804
898
|
"reasoning_steps": steps,
|
|
805
899
|
"tool_calls_count": tool_calls_count,
|
|
806
900
|
"iterations": iteration + 1,
|
|
@@ -809,11 +903,11 @@ class HybridAgent(BaseAIAgent):
|
|
|
809
903
|
}
|
|
810
904
|
return
|
|
811
905
|
|
|
812
|
-
# Check if tool call
|
|
813
|
-
if
|
|
906
|
+
# Check if tool call (ReAct mode, outside tags only)
|
|
907
|
+
if self._has_tool_call(thought_raw):
|
|
814
908
|
# ACT: Execute tool
|
|
815
909
|
try:
|
|
816
|
-
tool_info = self._parse_tool_call(
|
|
910
|
+
tool_info = self._parse_tool_call(thought_raw) # Parse from raw text
|
|
817
911
|
tool_name = tool_info.get("tool", "")
|
|
818
912
|
if not tool_name:
|
|
819
913
|
raise ValueError("Tool name not found in tool call")
|
|
@@ -834,27 +928,19 @@ class HybridAgent(BaseAIAgent):
|
|
|
834
928
|
)
|
|
835
929
|
tool_calls_count += 1
|
|
836
930
|
|
|
931
|
+
# Wrap tool call and result in step
|
|
837
932
|
steps.append(
|
|
838
933
|
{
|
|
839
934
|
"type": "action",
|
|
840
935
|
"tool": tool_info["tool"],
|
|
841
936
|
"operation": tool_info.get("operation"),
|
|
842
937
|
"parameters": tool_info.get("parameters"),
|
|
938
|
+
"result": str(tool_result), # Include result in step
|
|
843
939
|
"iteration": iteration + 1,
|
|
844
940
|
}
|
|
845
941
|
)
|
|
846
942
|
|
|
847
|
-
#
|
|
848
|
-
observation = f"OBSERVATION: Tool '{tool_info['tool']}' returned: {tool_result}"
|
|
849
|
-
steps.append(
|
|
850
|
-
{
|
|
851
|
-
"type": "observation",
|
|
852
|
-
"content": observation,
|
|
853
|
-
"iteration": iteration + 1,
|
|
854
|
-
}
|
|
855
|
-
)
|
|
856
|
-
|
|
857
|
-
# Yield tool result event
|
|
943
|
+
# Yield tool result event (streaming)
|
|
858
944
|
yield {
|
|
859
945
|
"type": "tool_result",
|
|
860
946
|
"tool_name": tool_name,
|
|
@@ -862,16 +948,22 @@ class HybridAgent(BaseAIAgent):
|
|
|
862
948
|
"timestamp": datetime.utcnow().isoformat(),
|
|
863
949
|
}
|
|
864
950
|
|
|
951
|
+
# OBSERVE: Add tool result to conversation (for LLM consumption)
|
|
952
|
+
observation_content = f"Tool '{tool_info['tool']}' returned: {tool_result}"
|
|
953
|
+
observation = f"<OBSERVATION>\n{observation_content}\n</OBSERVATION>"
|
|
954
|
+
|
|
865
955
|
# Add to messages for next iteration
|
|
866
|
-
messages.append(LLMMessage(role="assistant", content=
|
|
956
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
867
957
|
messages.append(LLMMessage(role="user", content=observation))
|
|
868
958
|
|
|
869
959
|
except Exception as e:
|
|
870
|
-
|
|
960
|
+
error_content = f"Tool execution failed: {str(e)}"
|
|
961
|
+
error_msg = f"<OBSERVATION>\n{error_content}\n</OBSERVATION>"
|
|
871
962
|
steps.append(
|
|
872
963
|
{
|
|
873
|
-
"type": "
|
|
874
|
-
"
|
|
964
|
+
"type": "action",
|
|
965
|
+
"tool": tool_name if "tool_name" in locals() else "unknown",
|
|
966
|
+
"error": str(e),
|
|
875
967
|
"iteration": iteration + 1,
|
|
876
968
|
"error": True,
|
|
877
969
|
}
|
|
@@ -885,22 +977,37 @@ class HybridAgent(BaseAIAgent):
|
|
|
885
977
|
"timestamp": datetime.utcnow().isoformat(),
|
|
886
978
|
}
|
|
887
979
|
|
|
888
|
-
messages.append(LLMMessage(role="assistant", content=
|
|
980
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
889
981
|
messages.append(LLMMessage(role="user", content=error_msg))
|
|
890
982
|
|
|
891
983
|
else:
|
|
892
|
-
#
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
984
|
+
# Check if there's an incomplete final response (has FINAL RESPONSE but no finish)
|
|
985
|
+
if self._has_incomplete_final_response(thought_raw):
|
|
986
|
+
# Incomplete final response - ask LLM to continue
|
|
987
|
+
continue_message = (
|
|
988
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
989
|
+
f"remaining: {self._max_iterations - iteration - 1}]\n"
|
|
990
|
+
"Your FINAL RESPONSE appears incomplete (missing 'finish' suffix). "
|
|
991
|
+
"Please continue your response from where you left off and end with 'finish' "
|
|
992
|
+
"to indicate completion. If no 'finish' suffix, the system will continue iteration."
|
|
993
|
+
)
|
|
994
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
995
|
+
messages.append(LLMMessage(role="user", content=continue_message))
|
|
996
|
+
else:
|
|
997
|
+
# No tool call or final response detected - ask LLM to continue
|
|
998
|
+
continue_message = (
|
|
999
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
1000
|
+
f"remaining: {self._max_iterations - iteration - 1}]\n"
|
|
1001
|
+
"Continuing from your previous output. "
|
|
1002
|
+
"If your generation is incomplete, please continue from where you left off. "
|
|
1003
|
+
"If you decide to take action, ensure proper format:\n"
|
|
1004
|
+
"- Tool call: TOOL:, OPERATION:, PARAMETERS: (outside tags)\n"
|
|
1005
|
+
"- Final response: FINAL RESPONSE: <content> finish (outside tags)"
|
|
1006
|
+
)
|
|
1007
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
1008
|
+
messages.append(LLMMessage(role="user", content=continue_message))
|
|
1009
|
+
# Continue to next iteration
|
|
1010
|
+
continue
|
|
904
1011
|
|
|
905
1012
|
# Max iterations reached
|
|
906
1013
|
logger.warning(f"HybridAgent {self.agent_id} reached max iterations")
|
|
@@ -937,6 +1044,16 @@ class HybridAgent(BaseAIAgent):
|
|
|
937
1044
|
for iteration in range(self._max_iterations):
|
|
938
1045
|
logger.debug(f"HybridAgent {self.agent_id} - ReAct iteration {iteration + 1}")
|
|
939
1046
|
|
|
1047
|
+
# Add iteration info to messages (except first iteration which has task context)
|
|
1048
|
+
if iteration > 0:
|
|
1049
|
+
iteration_info = (
|
|
1050
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
1051
|
+
f"remaining: {self._max_iterations - iteration - 1}]"
|
|
1052
|
+
)
|
|
1053
|
+
# Only add if the last message is not already an iteration info
|
|
1054
|
+
if messages and not messages[-1].content.startswith("[Iteration"):
|
|
1055
|
+
messages.append(LLMMessage(role="user", content=iteration_info))
|
|
1056
|
+
|
|
940
1057
|
# THINK: LLM reasons about next action
|
|
941
1058
|
# Use Function Calling if supported, otherwise use ReAct mode
|
|
942
1059
|
if self._use_function_calling and self._tool_schemas:
|
|
@@ -959,9 +1076,29 @@ class HybridAgent(BaseAIAgent):
|
|
|
959
1076
|
max_tokens=self._config.max_tokens,
|
|
960
1077
|
)
|
|
961
1078
|
|
|
962
|
-
|
|
1079
|
+
thought_raw = response.content or ""
|
|
963
1080
|
total_tokens += getattr(response, "total_tokens", 0)
|
|
964
1081
|
|
|
1082
|
+
# Update prompt cache metrics from LLM response
|
|
1083
|
+
cache_read_tokens = getattr(response, "cache_read_tokens", None)
|
|
1084
|
+
cache_creation_tokens = getattr(response, "cache_creation_tokens", None)
|
|
1085
|
+
cache_hit = getattr(response, "cache_hit", None)
|
|
1086
|
+
if cache_read_tokens is not None or cache_creation_tokens is not None or cache_hit is not None:
|
|
1087
|
+
self.update_cache_metrics(
|
|
1088
|
+
cache_read_tokens=cache_read_tokens,
|
|
1089
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
1090
|
+
cache_hit=cache_hit,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
# Store raw output in steps (no format processing)
|
|
1094
|
+
steps.append(
|
|
1095
|
+
{
|
|
1096
|
+
"type": "thought",
|
|
1097
|
+
"content": thought_raw.strip(), # Return raw output without processing
|
|
1098
|
+
"iteration": iteration + 1,
|
|
1099
|
+
}
|
|
1100
|
+
)
|
|
1101
|
+
|
|
965
1102
|
# Check for Function Calling response
|
|
966
1103
|
tool_calls = getattr(response, "tool_calls", None)
|
|
967
1104
|
function_call = getattr(response, "function_call", None)
|
|
@@ -989,52 +1126,50 @@ class HybridAgent(BaseAIAgent):
|
|
|
989
1126
|
func_args = tool_call["function"]["arguments"]
|
|
990
1127
|
|
|
991
1128
|
# Parse function name to extract tool and operation
|
|
992
|
-
#
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
tool_name
|
|
996
|
-
|
|
997
|
-
|
|
1129
|
+
# CRITICAL: Try exact match first, then fall back to underscore parsing
|
|
1130
|
+
if self._tool_instances and func_name in self._tool_instances:
|
|
1131
|
+
# Exact match found - use full function name as tool name
|
|
1132
|
+
tool_name = func_name
|
|
1133
|
+
operation = None
|
|
1134
|
+
elif self._available_tools and func_name in self._available_tools:
|
|
1135
|
+
# Exact match in available tools list
|
|
1136
|
+
tool_name = func_name
|
|
998
1137
|
operation = None
|
|
1138
|
+
else:
|
|
1139
|
+
# Fallback: try underscore parsing for legacy compatibility
|
|
1140
|
+
parts = func_name.split("_", 1)
|
|
1141
|
+
if len(parts) == 2:
|
|
1142
|
+
tool_name, operation = parts
|
|
1143
|
+
else:
|
|
1144
|
+
tool_name = parts[0]
|
|
1145
|
+
operation = None
|
|
999
1146
|
|
|
1000
1147
|
# Parse arguments JSON
|
|
1001
1148
|
import json
|
|
1002
1149
|
if isinstance(func_args, str):
|
|
1003
1150
|
parameters = json.loads(func_args)
|
|
1004
1151
|
else:
|
|
1005
|
-
parameters = func_args
|
|
1006
|
-
|
|
1007
|
-
steps.append(
|
|
1008
|
-
{
|
|
1009
|
-
"type": "thought",
|
|
1010
|
-
"content": f"Calling tool {func_name}",
|
|
1011
|
-
"iteration": iteration + 1,
|
|
1012
|
-
}
|
|
1013
|
-
)
|
|
1152
|
+
parameters = func_args if func_args else {}
|
|
1014
1153
|
|
|
1015
1154
|
# Execute tool
|
|
1016
1155
|
tool_result = await self._execute_tool(tool_name, operation, parameters)
|
|
1017
1156
|
tool_calls_count += 1
|
|
1018
1157
|
|
|
1158
|
+
# Wrap tool call and result in step
|
|
1019
1159
|
steps.append(
|
|
1020
1160
|
{
|
|
1021
1161
|
"type": "action",
|
|
1022
1162
|
"tool": tool_name,
|
|
1023
1163
|
"operation": operation,
|
|
1024
1164
|
"parameters": parameters,
|
|
1165
|
+
"result": str(tool_result), # Include result in step
|
|
1025
1166
|
"iteration": iteration + 1,
|
|
1026
1167
|
}
|
|
1027
1168
|
)
|
|
1028
1169
|
|
|
1029
|
-
# Add tool result to messages
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
{
|
|
1033
|
-
"type": "observation",
|
|
1034
|
-
"content": observation,
|
|
1035
|
-
"iteration": iteration + 1,
|
|
1036
|
-
}
|
|
1037
|
-
)
|
|
1170
|
+
# Add tool result to messages (for LLM consumption)
|
|
1171
|
+
observation_content = f"Tool '{tool_name}' returned: {tool_result}"
|
|
1172
|
+
observation = f"<OBSERVATION>\n{observation_content}\n</OBSERVATION>"
|
|
1038
1173
|
|
|
1039
1174
|
# Add assistant message with tool call and tool result
|
|
1040
1175
|
messages.append(
|
|
@@ -1053,13 +1188,14 @@ class HybridAgent(BaseAIAgent):
|
|
|
1053
1188
|
)
|
|
1054
1189
|
|
|
1055
1190
|
except Exception as e:
|
|
1056
|
-
|
|
1191
|
+
error_content = f"Tool execution failed: {str(e)}"
|
|
1192
|
+
error_msg = f"<OBSERVATION>\n{error_content}\n</OBSERVATION>"
|
|
1057
1193
|
steps.append(
|
|
1058
1194
|
{
|
|
1059
1195
|
"type": "observation",
|
|
1060
1196
|
"content": error_msg,
|
|
1061
1197
|
"iteration": iteration + 1,
|
|
1062
|
-
"
|
|
1198
|
+
"has_error": True,
|
|
1063
1199
|
}
|
|
1064
1200
|
)
|
|
1065
1201
|
# Add error to messages
|
|
@@ -1074,41 +1210,22 @@ class HybridAgent(BaseAIAgent):
|
|
|
1074
1210
|
# Continue to next iteration
|
|
1075
1211
|
continue
|
|
1076
1212
|
|
|
1077
|
-
#
|
|
1078
|
-
if self.
|
|
1079
|
-
|
|
1213
|
+
# Check for final response (outside tags only)
|
|
1214
|
+
if self._has_final_response(thought_raw):
|
|
1215
|
+
final_response = self._extract_final_response(thought_raw)
|
|
1080
1216
|
return {
|
|
1081
|
-
"
|
|
1217
|
+
"final_response": final_response, # Return raw output without processing
|
|
1082
1218
|
"steps": steps,
|
|
1083
1219
|
"iterations": iteration + 1,
|
|
1084
1220
|
"tool_calls_count": tool_calls_count,
|
|
1085
1221
|
"total_tokens": total_tokens,
|
|
1086
1222
|
}
|
|
1087
1223
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
"type": "thought",
|
|
1091
|
-
"content": thought,
|
|
1092
|
-
"iteration": iteration + 1,
|
|
1093
|
-
}
|
|
1094
|
-
)
|
|
1095
|
-
|
|
1096
|
-
# Check if final answer (ReAct mode)
|
|
1097
|
-
if "FINAL ANSWER:" in thought:
|
|
1098
|
-
final_answer = self._extract_final_answer(thought)
|
|
1099
|
-
return {
|
|
1100
|
-
"final_answer": final_answer,
|
|
1101
|
-
"steps": steps,
|
|
1102
|
-
"iterations": iteration + 1,
|
|
1103
|
-
"tool_calls_count": tool_calls_count,
|
|
1104
|
-
"total_tokens": total_tokens,
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
# Check if tool call (ReAct mode)
|
|
1108
|
-
if "TOOL:" in thought:
|
|
1224
|
+
# Check if tool call (ReAct mode, outside tags only)
|
|
1225
|
+
if self._has_tool_call(thought_raw):
|
|
1109
1226
|
# ACT: Execute tool
|
|
1110
1227
|
try:
|
|
1111
|
-
tool_info = self._parse_tool_call(
|
|
1228
|
+
tool_info = self._parse_tool_call(thought_raw) # Parse from raw text
|
|
1112
1229
|
tool_name = tool_info.get("tool", "")
|
|
1113
1230
|
if not tool_name:
|
|
1114
1231
|
raise ValueError("Tool name not found in tool call")
|
|
@@ -1119,57 +1236,74 @@ class HybridAgent(BaseAIAgent):
|
|
|
1119
1236
|
)
|
|
1120
1237
|
tool_calls_count += 1
|
|
1121
1238
|
|
|
1239
|
+
# Wrap tool call and result in step
|
|
1122
1240
|
steps.append(
|
|
1123
1241
|
{
|
|
1124
1242
|
"type": "action",
|
|
1125
1243
|
"tool": tool_info["tool"],
|
|
1126
1244
|
"operation": tool_info.get("operation"),
|
|
1127
1245
|
"parameters": tool_info.get("parameters"),
|
|
1246
|
+
"result": str(tool_result), # Include result in step
|
|
1128
1247
|
"iteration": iteration + 1,
|
|
1129
1248
|
}
|
|
1130
1249
|
)
|
|
1131
1250
|
|
|
1132
|
-
# OBSERVE: Add tool result to conversation
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
{
|
|
1136
|
-
"type": "observation",
|
|
1137
|
-
"content": observation,
|
|
1138
|
-
"iteration": iteration + 1,
|
|
1139
|
-
}
|
|
1140
|
-
)
|
|
1251
|
+
# OBSERVE: Add tool result to conversation (for LLM consumption)
|
|
1252
|
+
observation_content = f"Tool '{tool_info['tool']}' returned: {tool_result}"
|
|
1253
|
+
observation = f"<OBSERVATION>\n{observation_content}\n</OBSERVATION>"
|
|
1141
1254
|
|
|
1142
1255
|
# Add to messages for next iteration
|
|
1143
|
-
messages.append(LLMMessage(role="assistant", content=
|
|
1256
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
1144
1257
|
messages.append(LLMMessage(role="user", content=observation))
|
|
1145
1258
|
|
|
1146
1259
|
except Exception as e:
|
|
1147
|
-
|
|
1260
|
+
error_content = f"Tool execution failed: {str(e)}"
|
|
1261
|
+
error_msg = f"<OBSERVATION>\n{error_content}\n</OBSERVATION>"
|
|
1148
1262
|
steps.append(
|
|
1149
1263
|
{
|
|
1150
|
-
"type": "
|
|
1151
|
-
"
|
|
1264
|
+
"type": "action",
|
|
1265
|
+
"tool": tool_name if "tool_name" in locals() else "unknown",
|
|
1266
|
+
"error": str(e),
|
|
1152
1267
|
"iteration": iteration + 1,
|
|
1153
|
-
"
|
|
1268
|
+
"has_error": True,
|
|
1154
1269
|
}
|
|
1155
1270
|
)
|
|
1156
|
-
messages.append(LLMMessage(role="assistant", content=
|
|
1271
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
1157
1272
|
messages.append(LLMMessage(role="user", content=error_msg))
|
|
1158
1273
|
|
|
1159
1274
|
else:
|
|
1160
|
-
#
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1275
|
+
# Check if there's an incomplete final response (has FINAL RESPONSE but no finish)
|
|
1276
|
+
if self._has_incomplete_final_response(thought_raw):
|
|
1277
|
+
# Incomplete final response - ask LLM to continue
|
|
1278
|
+
continue_message = (
|
|
1279
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
1280
|
+
f"remaining: {self._max_iterations - iteration - 1}]\n"
|
|
1281
|
+
"Your FINAL RESPONSE appears incomplete (missing 'finish' suffix). "
|
|
1282
|
+
"Please continue your response from where you left off and end with 'finish' "
|
|
1283
|
+
"to indicate completion. If no 'finish' suffix, the system will continue iteration."
|
|
1284
|
+
)
|
|
1285
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
1286
|
+
messages.append(LLMMessage(role="user", content=continue_message))
|
|
1287
|
+
else:
|
|
1288
|
+
# No tool call or final response detected - ask LLM to continue
|
|
1289
|
+
continue_message = (
|
|
1290
|
+
f"[Iteration {iteration + 1}/{self._max_iterations}, "
|
|
1291
|
+
f"remaining: {self._max_iterations - iteration - 1}]\n"
|
|
1292
|
+
"Continuing from your previous output. "
|
|
1293
|
+
"If your generation is incomplete, please continue from where you left off. "
|
|
1294
|
+
"If you decide to take action, ensure proper format:\n"
|
|
1295
|
+
"- Tool call: TOOL:, OPERATION:, PARAMETERS: (outside tags)\n"
|
|
1296
|
+
"- Final response: FINAL RESPONSE: <content> finish (outside tags)"
|
|
1297
|
+
)
|
|
1298
|
+
messages.append(LLMMessage(role="assistant", content=thought_raw))
|
|
1299
|
+
messages.append(LLMMessage(role="user", content=continue_message))
|
|
1300
|
+
# Continue to next iteration
|
|
1301
|
+
continue
|
|
1168
1302
|
|
|
1169
1303
|
# Max iterations reached
|
|
1170
1304
|
logger.warning(f"HybridAgent {self.agent_id} reached max iterations")
|
|
1171
1305
|
return {
|
|
1172
|
-
"
|
|
1306
|
+
"final_response": "Max iterations reached. Unable to complete task fully.",
|
|
1173
1307
|
"steps": steps,
|
|
1174
1308
|
"iterations": self._max_iterations,
|
|
1175
1309
|
"tool_calls_count": tool_calls_count,
|
|
@@ -1198,17 +1332,42 @@ class HybridAgent(BaseAIAgent):
|
|
|
1198
1332
|
|
|
1199
1333
|
# Add context if provided
|
|
1200
1334
|
if context:
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1335
|
+
# Special handling: if context contains 'history' as a list of messages,
|
|
1336
|
+
# add them as separate user/assistant messages instead of formatting
|
|
1337
|
+
history = context.get("history")
|
|
1338
|
+
if isinstance(history, list) and len(history) > 0:
|
|
1339
|
+
# Check if history contains message-like dictionaries
|
|
1340
|
+
for msg in history:
|
|
1341
|
+
if isinstance(msg, dict) and "role" in msg and "content" in msg:
|
|
1342
|
+
# Valid message format - add as separate message
|
|
1343
|
+
messages.append(
|
|
1344
|
+
LLMMessage(
|
|
1345
|
+
role=msg["role"],
|
|
1346
|
+
content=msg["content"],
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
elif isinstance(msg, LLMMessage):
|
|
1350
|
+
# Already an LLMMessage instance
|
|
1351
|
+
messages.append(msg)
|
|
1352
|
+
|
|
1353
|
+
# Format remaining context fields (excluding history) as Additional Context
|
|
1354
|
+
context_without_history = {k: v for k, v in context.items() if k != "history"}
|
|
1355
|
+
if context_without_history:
|
|
1356
|
+
context_str = self._format_context(context_without_history)
|
|
1357
|
+
if context_str:
|
|
1358
|
+
messages.append(
|
|
1359
|
+
LLMMessage(
|
|
1360
|
+
role="user",
|
|
1361
|
+
content=f"Additional Context:\n{context_str}",
|
|
1362
|
+
)
|
|
1207
1363
|
)
|
|
1208
|
-
)
|
|
1209
1364
|
|
|
1210
|
-
# Add task
|
|
1211
|
-
|
|
1365
|
+
# Add task with iteration info
|
|
1366
|
+
task_message = (
|
|
1367
|
+
f"Task: {task}\n\n"
|
|
1368
|
+
f"[Iteration 1/{self._max_iterations}, remaining: {self._max_iterations - 1}]"
|
|
1369
|
+
)
|
|
1370
|
+
messages.append(LLMMessage(role="user", content=task_message))
|
|
1212
1371
|
|
|
1213
1372
|
return messages
|
|
1214
1373
|
|
|
@@ -1220,15 +1379,147 @@ class HybridAgent(BaseAIAgent):
|
|
|
1220
1379
|
relevant_fields.append(f"{key}: {value}")
|
|
1221
1380
|
return "\n".join(relevant_fields) if relevant_fields else ""
|
|
1222
1381
|
|
|
1223
|
-
def
|
|
1224
|
-
"""
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1382
|
+
def _extract_thought_content(self, text: str) -> str:
|
|
1383
|
+
"""
|
|
1384
|
+
Extract content from <THOUGHT>...</THOUGHT> tags.
|
|
1385
|
+
|
|
1386
|
+
DEPRECATED: This method is kept for backward compatibility but no longer
|
|
1387
|
+
extracts content. Returns original text as-is per new design.
|
|
1388
|
+
|
|
1389
|
+
Args:
|
|
1390
|
+
text: Text that may contain THOUGHT tags
|
|
1391
|
+
|
|
1392
|
+
Returns:
|
|
1393
|
+
Original text (no extraction performed)
|
|
1394
|
+
"""
|
|
1395
|
+
# Return original text without processing (new design)
|
|
1396
|
+
return text.strip()
|
|
1397
|
+
|
|
1398
|
+
def _extract_observation_content(self, text: str) -> str:
|
|
1399
|
+
"""
|
|
1400
|
+
Extract content from <OBSERVATION>...</OBSERVATION> tags.
|
|
1401
|
+
|
|
1402
|
+
DEPRECATED: This method is kept for backward compatibility but no longer
|
|
1403
|
+
extracts content. Returns original text as-is per new design.
|
|
1404
|
+
|
|
1405
|
+
Args:
|
|
1406
|
+
text: Text that may contain OBSERVATION tags
|
|
1407
|
+
|
|
1408
|
+
Returns:
|
|
1409
|
+
Original text (no extraction performed)
|
|
1410
|
+
"""
|
|
1411
|
+
# Return original text without processing (new design)
|
|
1412
|
+
return text.strip()
|
|
1228
1413
|
|
|
1229
|
-
def
|
|
1414
|
+
def _has_final_response(self, text: str) -> bool:
|
|
1415
|
+
"""
|
|
1416
|
+
Check if text contains complete FINAL RESPONSE with 'finish' suffix.
|
|
1417
|
+
|
|
1418
|
+
The FINAL RESPONSE must end with 'finish' to be considered complete.
|
|
1419
|
+
If FINAL RESPONSE is present but without 'finish', it's considered incomplete
|
|
1420
|
+
and the loop will continue to let LLM complete the response.
|
|
1421
|
+
|
|
1422
|
+
Args:
|
|
1423
|
+
text: Text to check
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
True if complete FINAL RESPONSE (with finish suffix) found outside tags
|
|
1427
|
+
"""
|
|
1428
|
+
import re
|
|
1429
|
+
|
|
1430
|
+
# Remove content inside THOUGHT and OBSERVATION tags
|
|
1431
|
+
text_without_tags = re.sub(r'<THOUGHT>.*?</THOUGHT>', '', text, flags=re.DOTALL)
|
|
1432
|
+
text_without_tags = re.sub(r'<OBSERVATION>.*?</OBSERVATION>', '', text_without_tags, flags=re.DOTALL)
|
|
1433
|
+
|
|
1434
|
+
# Check for FINAL RESPONSE marker with 'finish' suffix in remaining text
|
|
1435
|
+
# The 'finish' must appear after FINAL RESPONSE: content
|
|
1436
|
+
if "FINAL RESPONSE:" not in text_without_tags:
|
|
1437
|
+
return False
|
|
1438
|
+
|
|
1439
|
+
# Check if 'finish' appears after FINAL RESPONSE:
|
|
1440
|
+
# Use case-insensitive search for 'finish' at the end
|
|
1441
|
+
text_lower = text_without_tags.lower()
|
|
1442
|
+
final_response_idx = text_lower.find("final response:")
|
|
1443
|
+
if final_response_idx == -1:
|
|
1444
|
+
return False
|
|
1445
|
+
|
|
1446
|
+
# Check if 'finish' appears after the FINAL RESPONSE marker
|
|
1447
|
+
remaining_text = text_without_tags[final_response_idx:]
|
|
1448
|
+
return "finish" in remaining_text.lower()
|
|
1449
|
+
|
|
1450
|
+
def _has_incomplete_final_response(self, text: str) -> bool:
|
|
1230
1451
|
"""
|
|
1231
|
-
|
|
1452
|
+
Check if text contains FINAL RESPONSE marker but without 'finish' suffix.
|
|
1453
|
+
|
|
1454
|
+
Args:
|
|
1455
|
+
text: Text to check
|
|
1456
|
+
|
|
1457
|
+
Returns:
|
|
1458
|
+
True if FINAL RESPONSE marker found but without finish suffix
|
|
1459
|
+
"""
|
|
1460
|
+
import re
|
|
1461
|
+
|
|
1462
|
+
# Remove content inside THOUGHT and OBSERVATION tags
|
|
1463
|
+
text_without_tags = re.sub(r'<THOUGHT>.*?</THOUGHT>', '', text, flags=re.DOTALL)
|
|
1464
|
+
text_without_tags = re.sub(r'<OBSERVATION>.*?</OBSERVATION>', '', text_without_tags, flags=re.DOTALL)
|
|
1465
|
+
|
|
1466
|
+
# Check for FINAL RESPONSE marker without 'finish' suffix
|
|
1467
|
+
if "FINAL RESPONSE:" not in text_without_tags:
|
|
1468
|
+
return False
|
|
1469
|
+
|
|
1470
|
+
# Check if 'finish' is missing
|
|
1471
|
+
text_lower = text_without_tags.lower()
|
|
1472
|
+
final_response_idx = text_lower.find("final response:")
|
|
1473
|
+
remaining_text = text_without_tags[final_response_idx:]
|
|
1474
|
+
return "finish" not in remaining_text.lower()
|
|
1475
|
+
|
|
1476
|
+
def _extract_final_response(self, text: str) -> str:
|
|
1477
|
+
"""
|
|
1478
|
+
Extract final response from text, preserving original format.
|
|
1479
|
+
Only extracts from outside THOUGHT/OBSERVATION tags.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
text: Text that may contain FINAL RESPONSE marker
|
|
1483
|
+
|
|
1484
|
+
Returns:
|
|
1485
|
+
Original text if FINAL RESPONSE found, otherwise empty string
|
|
1486
|
+
"""
|
|
1487
|
+
import re
|
|
1488
|
+
|
|
1489
|
+
# Remove content inside THOUGHT and OBSERVATION tags
|
|
1490
|
+
text_without_tags = re.sub(r'<THOUGHT>.*?</THOUGHT>', '', text, flags=re.DOTALL)
|
|
1491
|
+
text_without_tags = re.sub(r'<OBSERVATION>.*?</OBSERVATION>', '', text_without_tags, flags=re.DOTALL)
|
|
1492
|
+
|
|
1493
|
+
# Check for FINAL RESPONSE marker
|
|
1494
|
+
if "FINAL RESPONSE:" in text_without_tags:
|
|
1495
|
+
# Return original text without any processing
|
|
1496
|
+
return text.strip()
|
|
1497
|
+
|
|
1498
|
+
return ""
|
|
1499
|
+
|
|
1500
|
+
def _has_tool_call(self, text: str) -> bool:
|
|
1501
|
+
"""
|
|
1502
|
+
Check if text contains TOOL call marker outside of THOUGHT/OBSERVATION tags.
|
|
1503
|
+
|
|
1504
|
+
Args:
|
|
1505
|
+
text: Text to check
|
|
1506
|
+
|
|
1507
|
+
Returns:
|
|
1508
|
+
True if TOOL marker found outside tags
|
|
1509
|
+
"""
|
|
1510
|
+
import re
|
|
1511
|
+
|
|
1512
|
+
# Remove content inside THOUGHT and OBSERVATION tags
|
|
1513
|
+
text_without_tags = re.sub(r'<THOUGHT>.*?</THOUGHT>', '', text, flags=re.DOTALL)
|
|
1514
|
+
text_without_tags = re.sub(r'<OBSERVATION>.*?</OBSERVATION>', '', text_without_tags, flags=re.DOTALL)
|
|
1515
|
+
|
|
1516
|
+
# Check for TOOL marker in remaining text
|
|
1517
|
+
return "TOOL:" in text_without_tags
|
|
1518
|
+
|
|
1519
|
+
def _parse_tool_call(self, text: str) -> Dict[str, Any]:
|
|
1520
|
+
"""
|
|
1521
|
+
Parse tool call from LLM output.
|
|
1522
|
+
Only parses from outside THOUGHT/OBSERVATION tags.
|
|
1232
1523
|
|
|
1233
1524
|
Expected format:
|
|
1234
1525
|
TOOL: <tool_name>
|
|
@@ -1236,28 +1527,33 @@ class HybridAgent(BaseAIAgent):
|
|
|
1236
1527
|
PARAMETERS: <json_parameters>
|
|
1237
1528
|
|
|
1238
1529
|
Args:
|
|
1239
|
-
|
|
1530
|
+
text: LLM output that may contain tool call
|
|
1240
1531
|
|
|
1241
1532
|
Returns:
|
|
1242
1533
|
Dictionary with 'tool', 'operation', 'parameters'
|
|
1243
1534
|
"""
|
|
1244
1535
|
import json
|
|
1536
|
+
import re
|
|
1245
1537
|
|
|
1246
1538
|
result = {}
|
|
1539
|
+
|
|
1540
|
+
# Remove content inside THOUGHT and OBSERVATION tags
|
|
1541
|
+
text_without_tags = re.sub(r'<THOUGHT>.*?</THOUGHT>', '', text, flags=re.DOTALL)
|
|
1542
|
+
text_without_tags = re.sub(r'<OBSERVATION>.*?</OBSERVATION>', '', text_without_tags, flags=re.DOTALL)
|
|
1247
1543
|
|
|
1248
|
-
# Extract tool
|
|
1249
|
-
if "TOOL:" in
|
|
1250
|
-
tool_line = [line for line in
|
|
1544
|
+
# Extract tool from text outside tags
|
|
1545
|
+
if "TOOL:" in text_without_tags:
|
|
1546
|
+
tool_line = [line for line in text_without_tags.split("\n") if line.strip().startswith("TOOL:")][0]
|
|
1251
1547
|
result["tool"] = tool_line.split("TOOL:", 1)[1].strip()
|
|
1252
1548
|
|
|
1253
1549
|
# Extract operation (optional)
|
|
1254
|
-
if "OPERATION:" in
|
|
1255
|
-
op_line = [line for line in
|
|
1550
|
+
if "OPERATION:" in text_without_tags:
|
|
1551
|
+
op_line = [line for line in text_without_tags.split("\n") if line.strip().startswith("OPERATION:")][0]
|
|
1256
1552
|
result["operation"] = op_line.split("OPERATION:", 1)[1].strip()
|
|
1257
1553
|
|
|
1258
1554
|
# Extract parameters (optional)
|
|
1259
|
-
if "PARAMETERS:" in
|
|
1260
|
-
param_line = [line for line in
|
|
1555
|
+
if "PARAMETERS:" in text_without_tags:
|
|
1556
|
+
param_line = [line for line in text_without_tags.split("\n") if line.strip().startswith("PARAMETERS:")][0]
|
|
1261
1557
|
param_str = param_line.split("PARAMETERS:", 1)[1].strip()
|
|
1262
1558
|
try:
|
|
1263
1559
|
result["parameters"] = json.loads(param_str)
|