hammad-python 0.0.23__py3-none-any.whl → 0.0.24__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.
@@ -210,6 +210,13 @@ def _update_context_object(
210
210
  raise ValueError(f"Cannot update context of type {type(context)}")
211
211
 
212
212
 
213
+ def mark_complete() -> None:
214
+ """If you feel you are ready to respond to the user, or have completed
215
+ the task given to you, call this function to mark your response as
216
+ complete."""
217
+ return "complete"
218
+
219
+
213
220
  class Agent(BaseGenAIModel, Generic[T]):
214
221
  """A generative AI agent that can execute tools, generate structured outputs,
215
222
  and maintain context across multiple conversation steps.
@@ -245,6 +252,11 @@ class Agent(BaseGenAIModel, Generic[T]):
245
252
  tools: Union[List[Tool], Callable, None] = None,
246
253
  settings: Optional[AgentSettings] = None,
247
254
  instructor_mode: Optional[LanguageModelInstructorMode] = None,
255
+ # Defaults
256
+ max_steps: int = 10,
257
+ # End Strategy
258
+ end_strategy: Literal["tool"] | None = None,
259
+ end_tool: Callable = mark_complete,
248
260
  # Context management parameters
249
261
  context_updates: Optional[
250
262
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
@@ -256,6 +268,8 @@ class Agent(BaseGenAIModel, Generic[T]):
256
268
  context_selection_instructions: Optional[str] = None,
257
269
  context_update_instructions: Optional[str] = None,
258
270
  context_format: Literal["json", "python", "markdown"] = "json",
271
+ verbose: bool = False,
272
+ debug: bool = False,
259
273
  **kwargs: Any,
260
274
  ):
261
275
  """Create a new AI agent with specified capabilities and behavior.
@@ -272,6 +286,11 @@ class Agent(BaseGenAIModel, Generic[T]):
272
286
  tools: List of tools/functions the agent can call, or a single callable
273
287
  settings: AgentSettings object to customize default behavior
274
288
  instructor_mode: Mode for structured output generation
289
+ max_steps: Default ,aximum number of steps the agent can take before stopping
290
+ end_strategy: Optional alternative strategy to provide an end tool for determining agent's final
291
+ response.
292
+ end_tool: The tool the agent will call to determine if it should stop.
293
+ This is only used if end_strategy is set to "tool".
275
294
  context_updates: When to update context - "before", "after", or both
276
295
  context_confirm: Whether to confirm context updates with the user
277
296
  context_strategy: How to select context updates - "selective" or "all"
@@ -280,6 +299,8 @@ class Agent(BaseGenAIModel, Generic[T]):
280
299
  context_selection_instructions: Custom instructions for context selection
281
300
  context_update_instructions: Custom instructions for context updates
282
301
  context_format: Format for context display - "json", "python", or "markdown"
302
+ verbose: If True, set logger to INFO level for detailed output
303
+ debug: If True, set logger to DEBUG level for maximum verbosity
283
304
  **kwargs: Additional parameters passed to the underlying language model
284
305
 
285
306
  Example:
@@ -307,6 +328,17 @@ class Agent(BaseGenAIModel, Generic[T]):
307
328
  self.settings = settings or AgentSettings()
308
329
  self.instructor_mode = instructor_mode
309
330
 
331
+ # Store max_steps as instance variable (overrides settings if provided)
332
+ self.max_steps = max_steps if max_steps is not None else self.settings.max_steps
333
+
334
+ # Store end strategy parameters
335
+ self.end_strategy = end_strategy
336
+ self.end_tool = end_tool if end_tool is not None else mark_complete
337
+
338
+ # Add end_tool to tools if end_strategy is 'tool'
339
+ if self.end_strategy == "tool":
340
+ self.tools.append(define_tool(self.end_tool))
341
+
310
342
  # Process instructions
311
343
  self.instructions = _get_instructions(
312
344
  name=name,
@@ -314,11 +346,23 @@ class Agent(BaseGenAIModel, Generic[T]):
314
346
  add_name_to_instructions=self.settings.add_name_to_instructions,
315
347
  )
316
348
 
349
+ # Store verbose/debug settings
350
+ self.verbose = verbose
351
+ self.debug = debug
352
+
353
+ # Set logger level based on verbose/debug flags
354
+ if debug:
355
+ logger.level = "debug"
356
+ elif verbose:
357
+ logger.level = "info"
358
+
317
359
  # Initialize the language model
318
360
  if isinstance(model, LanguageModel):
319
361
  self._language_model = model
320
362
  else:
321
- self._language_model = LanguageModel(model=model, **kwargs)
363
+ self._language_model = LanguageModel(
364
+ model=model, verbose=verbose, debug=debug, **kwargs
365
+ )
322
366
 
323
367
  # Context management settings
324
368
  self.context_updates = context_updates
@@ -689,6 +733,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
689
733
  max_steps: Optional[int] = None,
690
734
  context: Optional[AgentContext] = None,
691
735
  output_type: Optional[Type[T]] = None,
736
+ end_strategy: Optional[Literal["tool"]] = None,
737
+ end_tool: Optional[Callable] = None,
692
738
  context_updates: Optional[
693
739
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
694
740
  ] = None,
@@ -699,6 +745,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
699
745
  context_selection_instructions: Optional[str] = None,
700
746
  context_update_instructions: Optional[str] = None,
701
747
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
748
+ verbose: Optional[bool] = None,
749
+ debug: Optional[bool] = None,
702
750
  *,
703
751
  stream: Literal[False] = False,
704
752
  **kwargs: Any,
@@ -712,6 +760,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
712
760
  max_steps: Optional[int] = None,
713
761
  context: Optional[AgentContext] = None,
714
762
  output_type: Optional[Type[T]] = None,
763
+ end_strategy: Optional[Literal["tool"]] = None,
764
+ end_tool: Optional[Callable] = None,
715
765
  context_updates: Optional[
716
766
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
717
767
  ] = None,
@@ -722,6 +772,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
722
772
  context_selection_instructions: Optional[str] = None,
723
773
  context_update_instructions: Optional[str] = None,
724
774
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
775
+ verbose: Optional[bool] = None,
776
+ debug: Optional[bool] = None,
725
777
  *,
726
778
  stream: Literal[True],
727
779
  **kwargs: Any,
@@ -734,6 +786,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
734
786
  max_steps: Optional[int] = None,
735
787
  context: Optional[AgentContext] = None,
736
788
  output_type: Optional[Type[T]] = None,
789
+ end_strategy: Optional[Literal["tool"]] = None,
790
+ end_tool: Optional[Callable] = None,
737
791
  context_updates: Optional[
738
792
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
739
793
  ] = None,
@@ -744,6 +798,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
744
798
  context_selection_instructions: Optional[str] = None,
745
799
  context_update_instructions: Optional[str] = None,
746
800
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
801
+ verbose: Optional[bool] = None,
802
+ debug: Optional[bool] = None,
747
803
  stream: bool = False,
748
804
  **kwargs: Any,
749
805
  ) -> Union[AgentResponse[T, AgentContext], AgentStream[T, AgentContext]]:
@@ -831,132 +887,320 @@ Please update the appropriate fields based on the conversation. Only update fiel
831
887
  **kwargs,
832
888
  )
833
889
 
834
- # Use provided model or default
835
- if model is None:
836
- working_model = self.language_model
837
- elif isinstance(model, str):
838
- working_model = LanguageModel(model=model)
839
- else:
840
- working_model = model
841
-
842
- # Use provided max_steps or default
843
- if max_steps is None:
844
- max_steps = self.settings.max_steps
845
-
846
- # Get effective context settings
847
- effective_context_settings = self._get_effective_context_settings(
848
- context_updates=context_updates,
849
- context_confirm=context_confirm,
850
- context_strategy=context_strategy,
851
- context_max_retries=context_max_retries,
852
- context_confirm_instructions=context_confirm_instructions,
853
- context_selection_instructions=context_selection_instructions,
854
- context_update_instructions=context_update_instructions,
855
- context_format=context_format,
890
+ # Set logger level for this request if specified
891
+ original_level = logger.level
892
+ if debug or (debug is None and self.debug):
893
+ logger.level = "debug"
894
+ elif verbose or (verbose is None and self.verbose):
895
+ logger.level = "info"
896
+
897
+ # Log agent execution start
898
+ logger.info(f"Starting agent '{self.name}' execution")
899
+ logger.debug(
900
+ f"Agent settings: max_steps={max_steps or self.max_steps}, tools={len(self.tools)}"
856
901
  )
857
902
 
858
- # Parse initial messages
859
- parsed_messages = parse_messages(messages)
860
- current_messages = parsed_messages.copy()
861
- steps: List[LanguageModelResponse[str]] = []
862
-
863
- # RUN MAIN AGENTIC LOOP
864
- for step in range(max_steps):
865
- # Update context before processing if configured
866
- if context and self._should_update_context(
867
- context, "before", effective_context_settings["context_updates"]
868
- ):
869
- context = self._perform_context_update(
870
- context=context,
871
- model=working_model,
872
- current_messages=current_messages,
873
- timing="before",
874
- effective_settings=effective_context_settings,
875
- )
876
-
877
- # Format messages with instructions and context for first step only
878
- if step == 0:
879
- formatted_messages = self._format_messages_with_context(
880
- messages=current_messages,
881
- context=context,
903
+ try:
904
+ # Use provided model or default
905
+ if model is None:
906
+ working_model = self.language_model
907
+ elif isinstance(model, str):
908
+ working_model = LanguageModel(
909
+ model=model,
910
+ verbose=verbose or self.verbose,
911
+ debug=debug or self.debug,
882
912
  )
883
913
  else:
884
- formatted_messages = current_messages
885
-
886
- # Prepare kwargs for language model
887
- model_kwargs = kwargs.copy()
888
- if output_type:
889
- model_kwargs["type"] = output_type
890
- if self.instructor_mode:
891
- model_kwargs["instructor_mode"] = self.instructor_mode
892
-
893
- # Get language model response
894
- response = working_model.run(
895
- messages=formatted_messages,
896
- tools=[tool.to_dict() for tool in self.tools] if self.tools else None,
897
- **model_kwargs,
898
- )
914
+ working_model = model
899
915
 
900
- # Check if response has tool calls
901
- if response.has_tool_calls():
902
- # Add response to message history (with tool calls)
903
- current_messages.append(response.to_message())
916
+ # Use provided max_steps or default from instance
917
+ if max_steps is None:
918
+ max_steps = self.max_steps
904
919
 
905
- # Execute tools and add their responses to messages
906
- tool_responses = execute_tools_from_language_model_response(
907
- tools=self.tools, response=response
908
- )
909
- # Add tool responses to message history
910
- for tool_resp in tool_responses:
911
- current_messages.append(tool_resp.to_dict())
920
+ # Use provided end_strategy or default from instance
921
+ effective_end_strategy = (
922
+ end_strategy if end_strategy is not None else self.end_strategy
923
+ )
924
+ effective_end_tool = end_tool if end_tool is not None else self.end_tool
925
+
926
+ # Create working tools list with end_tool if needed
927
+ working_tools = self.tools.copy()
928
+ if effective_end_strategy == "tool" and effective_end_tool is not None:
929
+ end_tool_obj = define_tool(effective_end_tool)
930
+ # Only add if not already present
931
+ if not any(tool.name == end_tool_obj.name for tool in working_tools):
932
+ working_tools.append(end_tool_obj)
933
+
934
+ # Get effective context settings
935
+ effective_context_settings = self._get_effective_context_settings(
936
+ context_updates=context_updates,
937
+ context_confirm=context_confirm,
938
+ context_strategy=context_strategy,
939
+ context_max_retries=context_max_retries,
940
+ context_confirm_instructions=context_confirm_instructions,
941
+ context_selection_instructions=context_selection_instructions,
942
+ context_update_instructions=context_update_instructions,
943
+ context_format=context_format,
944
+ )
912
945
 
913
- # This is not the final step, add to steps
914
- steps.append(response)
915
- else:
916
- # No tool calls - this is the final step
917
- # Update context after processing if configured
946
+ # Parse initial messages
947
+ parsed_messages = parse_messages(messages)
948
+ current_messages = parsed_messages.copy()
949
+ steps: List[LanguageModelResponse[str]] = []
950
+
951
+ # RUN MAIN AGENTIC LOOP
952
+ logger.debug(f"Starting agentic loop with max_steps={max_steps}")
953
+ for step in range(max_steps):
954
+ logger.debug(f"Agent step {step + 1}/{max_steps}")
955
+ # Update context before processing if configured
918
956
  if context and self._should_update_context(
919
- context, "after", effective_context_settings["context_updates"]
957
+ context, "before", effective_context_settings["context_updates"]
920
958
  ):
921
959
  context = self._perform_context_update(
922
960
  context=context,
923
961
  model=working_model,
924
962
  current_messages=current_messages,
925
- timing="after",
963
+ timing="before",
926
964
  effective_settings=effective_context_settings,
927
965
  )
928
- return _create_agent_response_from_language_model_response(
929
- response=response, steps=steps, context=context
966
+
967
+ # Format messages with instructions and context for first step only
968
+ if step == 0:
969
+ formatted_messages = self._format_messages_with_context(
970
+ messages=current_messages,
971
+ context=context,
972
+ )
973
+ else:
974
+ formatted_messages = current_messages
975
+
976
+ # Prepare kwargs for language model
977
+ model_kwargs = kwargs.copy()
978
+ # Don't add output_type for intermediate steps - only for final response
979
+ if self.instructor_mode:
980
+ model_kwargs["instructor_mode"] = self.instructor_mode
981
+
982
+ # Get language model response
983
+ response = working_model.run(
984
+ messages=formatted_messages,
985
+ tools=[tool.to_dict() for tool in working_tools]
986
+ if working_tools
987
+ else None,
988
+ **model_kwargs,
930
989
  )
931
990
 
932
- # Max steps reached - return last response
933
- if steps:
934
- final_response = steps[-1]
935
- else:
936
- # No steps taken, make a final call
937
- final_response = working_model.run(
938
- messages=self._format_messages_with_context(
939
- messages=current_messages,
991
+ # Check if response has tool calls
992
+ if response.has_tool_calls():
993
+ logger.info(
994
+ f"Agent '{self.name}' making tool calls: {len(response.tool_calls)} tools"
995
+ )
996
+ for tool_call in response.tool_calls:
997
+ logger.debug(
998
+ f"Tool call: {tool_call.function.name}({tool_call.function.arguments})"
999
+ )
1000
+
1001
+ # Add response to message history (with tool calls)
1002
+ current_messages.append(response.to_message())
1003
+
1004
+ # Execute tools and add their responses to messages
1005
+ tool_responses = execute_tools_from_language_model_response(
1006
+ tools=working_tools, response=response
1007
+ )
1008
+ # Add tool responses to message history
1009
+ for tool_resp in tool_responses:
1010
+ current_messages.append(tool_resp.to_dict())
1011
+
1012
+ # This is not the final step, add to steps
1013
+ steps.append(response)
1014
+ else:
1015
+ # No tool calls - check if this is actually the final step based on end_strategy
1016
+ if effective_end_strategy == "tool":
1017
+ # Check if the end_tool was called
1018
+ end_tool_called = (
1019
+ any(
1020
+ tool_call.function.name == effective_end_tool.__name__
1021
+ for tool_call in response.tool_calls
1022
+ )
1023
+ if response.tool_calls
1024
+ else False
1025
+ )
1026
+
1027
+ if not end_tool_called:
1028
+ # End tool not called, continue the conversation
1029
+ logger.debug(
1030
+ f"Agent '{self.name}' step {step + 1}: No end tool called, continuing..."
1031
+ )
1032
+
1033
+ # Add the response to history
1034
+ current_messages.append(response.to_message())
1035
+
1036
+ # Add system message instructing agent to call the end tool
1037
+ current_messages.append(
1038
+ {
1039
+ "role": "system",
1040
+ "content": f"You must call the {effective_end_tool.__name__} tool to complete your response. Do not provide a final answer until you have called this tool.",
1041
+ }
1042
+ )
1043
+
1044
+ # Add user message to continue
1045
+ current_messages.append(
1046
+ {"role": "user", "content": "continue"}
1047
+ )
1048
+
1049
+ # Remove the continue message and append assistant content
1050
+ current_messages.pop() # Remove "continue" message
1051
+
1052
+ # This is not the final step, add to steps and continue
1053
+ steps.append(response)
1054
+ continue
1055
+
1056
+ # This is the final step (either no end_strategy or end_tool was called)
1057
+ logger.info(
1058
+ f"Agent '{self.name}' completed execution in {step + 1} steps"
1059
+ )
1060
+ # Now we can make the final call with the output_type if specified
1061
+ if output_type:
1062
+ # Make a final call with the structured output type
1063
+ final_model_kwargs = kwargs.copy()
1064
+ final_model_kwargs["type"] = output_type
1065
+ if self.instructor_mode:
1066
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1067
+
1068
+ # Create a clean conversation history for structured output
1069
+ # Include the original messages and the final response content
1070
+ clean_messages = []
1071
+ # Add original user messages (excluding tool calls/responses)
1072
+ for msg in formatted_messages:
1073
+ if isinstance(msg, dict) and msg.get("role") not in [
1074
+ "tool",
1075
+ "assistant",
1076
+ ]:
1077
+ clean_messages.append(msg)
1078
+ elif hasattr(msg, "role") and msg.role not in [
1079
+ "tool",
1080
+ "assistant",
1081
+ ]:
1082
+ clean_messages.append(msg.to_dict())
1083
+
1084
+ # Add the final assistant response content
1085
+ clean_messages.append(
1086
+ {"role": "assistant", "content": response.get_content()}
1087
+ )
1088
+
1089
+ # Use the clean conversation history to generate structured output
1090
+ final_response = working_model.run(
1091
+ messages=clean_messages,
1092
+ **final_model_kwargs,
1093
+ )
1094
+
1095
+ # Update context after processing if configured
1096
+ if context and self._should_update_context(
1097
+ context,
1098
+ "after",
1099
+ effective_context_settings["context_updates"],
1100
+ ):
1101
+ context = self._perform_context_update(
1102
+ context=context,
1103
+ model=working_model,
1104
+ current_messages=current_messages,
1105
+ timing="after",
1106
+ effective_settings=effective_context_settings,
1107
+ )
1108
+ return _create_agent_response_from_language_model_response(
1109
+ response=final_response, steps=steps, context=context
1110
+ )
1111
+ else:
1112
+ # Update context after processing if configured
1113
+ if context and self._should_update_context(
1114
+ context,
1115
+ "after",
1116
+ effective_context_settings["context_updates"],
1117
+ ):
1118
+ context = self._perform_context_update(
1119
+ context=context,
1120
+ model=working_model,
1121
+ current_messages=current_messages,
1122
+ timing="after",
1123
+ effective_settings=effective_context_settings,
1124
+ )
1125
+ return _create_agent_response_from_language_model_response(
1126
+ response=response, steps=steps, context=context
1127
+ )
1128
+
1129
+ # Max steps reached - return last response
1130
+ if steps:
1131
+ final_response = steps[-1]
1132
+ # If we have an output_type, make a final structured call
1133
+ if output_type:
1134
+ final_model_kwargs = kwargs.copy()
1135
+ final_model_kwargs["type"] = output_type
1136
+ if self.instructor_mode:
1137
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1138
+
1139
+ # Create clean messages for structured output
1140
+ clean_messages = []
1141
+ formatted_messages = self._format_messages_with_context(
1142
+ messages=current_messages,
1143
+ context=context,
1144
+ )
1145
+
1146
+ # Add original user messages (excluding tool calls/responses)
1147
+ for msg in formatted_messages:
1148
+ if isinstance(msg, dict) and msg.get("role") not in [
1149
+ "tool",
1150
+ "assistant",
1151
+ ]:
1152
+ clean_messages.append(msg)
1153
+ elif hasattr(msg, "role") and msg.role not in [
1154
+ "tool",
1155
+ "assistant",
1156
+ ]:
1157
+ clean_messages.append(msg.to_dict())
1158
+
1159
+ # Add final response content
1160
+ clean_messages.append(
1161
+ {"role": "assistant", "content": final_response.get_content()}
1162
+ )
1163
+
1164
+ final_response = working_model.run(
1165
+ messages=clean_messages,
1166
+ **final_model_kwargs,
1167
+ )
1168
+ else:
1169
+ # No steps taken, make a final call
1170
+ final_model_kwargs = kwargs.copy()
1171
+ if output_type:
1172
+ final_model_kwargs["type"] = output_type
1173
+ if self.instructor_mode:
1174
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1175
+
1176
+ final_response = working_model.run(
1177
+ messages=self._format_messages_with_context(
1178
+ messages=current_messages,
1179
+ context=context,
1180
+ ),
1181
+ **final_model_kwargs,
1182
+ )
1183
+
1184
+ # Update context after processing if configured
1185
+ if context and self._should_update_context(
1186
+ context, "after", effective_context_settings["context_updates"]
1187
+ ):
1188
+ context = self._perform_context_update(
940
1189
  context=context,
941
- ),
942
- **model_kwargs,
943
- )
1190
+ model=working_model,
1191
+ current_messages=current_messages,
1192
+ timing="after",
1193
+ effective_settings=effective_context_settings,
1194
+ )
944
1195
 
945
- # Update context after processing if configured
946
- if context and self._should_update_context(
947
- context, "after", effective_context_settings["context_updates"]
948
- ):
949
- context = self._perform_context_update(
950
- context=context,
951
- model=working_model,
952
- current_messages=current_messages,
953
- timing="after",
954
- effective_settings=effective_context_settings,
1196
+ return _create_agent_response_from_language_model_response(
1197
+ response=final_response, steps=steps, context=context
955
1198
  )
956
1199
 
957
- return _create_agent_response_from_language_model_response(
958
- response=final_response, steps=steps, context=context
959
- )
1200
+ finally:
1201
+ # Restore original logger level
1202
+ if debug is not None or verbose is not None:
1203
+ logger.level = original_level
960
1204
 
961
1205
  async def async_run(
962
1206
  self,
@@ -975,6 +1219,10 @@ Please update the appropriate fields based on the conversation. Only update fiel
975
1219
  context_selection_instructions: Optional[str] = None,
976
1220
  context_update_instructions: Optional[str] = None,
977
1221
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
1222
+ verbose: Optional[bool] = None,
1223
+ debug: Optional[bool] = None,
1224
+ end_strategy: Optional[Literal["tool"]] = None,
1225
+ end_tool: Optional[Callable] = None,
978
1226
  **kwargs: Any,
979
1227
  ) -> AgentResponse[T, AgentContext]:
980
1228
  """Runs this agent asynchronously and returns a final agent response.
@@ -1044,132 +1292,301 @@ Please update the appropriate fields based on the conversation. Only update fiel
1044
1292
  ... )
1045
1293
  ... return response.output
1046
1294
  """
1047
- # Use provided model or default
1048
- if model is None:
1049
- working_model = self.language_model
1050
- elif isinstance(model, str):
1051
- working_model = LanguageModel(model=model)
1052
- else:
1053
- working_model = model
1054
-
1055
- # Use provided max_steps or default
1056
- if max_steps is None:
1057
- max_steps = self.settings.max_steps
1058
-
1059
- # Get effective context settings
1060
- effective_context_settings = self._get_effective_context_settings(
1061
- context_updates=context_updates,
1062
- context_confirm=context_confirm,
1063
- context_strategy=context_strategy,
1064
- context_max_retries=context_max_retries,
1065
- context_confirm_instructions=context_confirm_instructions,
1066
- context_selection_instructions=context_selection_instructions,
1067
- context_update_instructions=context_update_instructions,
1068
- context_format=context_format,
1069
- )
1070
-
1071
- # Parse initial messages
1072
- parsed_messages = parse_messages(messages)
1073
- current_messages = parsed_messages.copy()
1074
- steps: List[LanguageModelResponse[str]] = []
1075
-
1076
- # RUN MAIN AGENTIC LOOP
1077
- for step in range(max_steps):
1078
- # Update context before processing if configured
1079
- if context and self._should_update_context(
1080
- context, "before", effective_context_settings["context_updates"]
1081
- ):
1082
- context = self._perform_context_update(
1083
- context=context,
1084
- model=working_model,
1085
- current_messages=current_messages,
1086
- timing="before",
1087
- effective_settings=effective_context_settings,
1088
- )
1089
-
1090
- # Format messages with instructions and context for first step only
1091
- if step == 0:
1092
- formatted_messages = self._format_messages_with_context(
1093
- messages=current_messages,
1094
- context=context,
1295
+ # Set logger level for this request if specified
1296
+ original_level = logger.level
1297
+ if debug or (debug is None and self.debug):
1298
+ logger.level = "debug"
1299
+ elif verbose or (verbose is None and self.verbose):
1300
+ logger.level = "info"
1301
+
1302
+ try:
1303
+ # Use provided model or default
1304
+ if model is None:
1305
+ working_model = self.language_model
1306
+ elif isinstance(model, str):
1307
+ working_model = LanguageModel(
1308
+ model=model,
1309
+ verbose=verbose or self.verbose,
1310
+ debug=debug or self.debug,
1095
1311
  )
1096
1312
  else:
1097
- formatted_messages = current_messages
1098
-
1099
- # Prepare kwargs for language model
1100
- model_kwargs = kwargs.copy()
1101
- if output_type:
1102
- model_kwargs["type"] = output_type
1103
- if self.instructor_mode:
1104
- model_kwargs["instructor_mode"] = self.instructor_mode
1105
-
1106
- # Get language model response
1107
- response = await working_model.async_run(
1108
- messages=formatted_messages,
1109
- tools=[tool.to_dict() for tool in self.tools] if self.tools else None,
1110
- **model_kwargs,
1111
- )
1313
+ working_model = model
1112
1314
 
1113
- # Check if response has tool calls
1114
- if response.has_tool_calls():
1115
- # Add response to message history (with tool calls)
1116
- current_messages.append(response.to_message())
1315
+ # Use provided max_steps or default from instance
1316
+ if max_steps is None:
1317
+ max_steps = self.max_steps
1117
1318
 
1118
- # Execute tools and add their responses to messages
1119
- tool_responses = execute_tools_from_language_model_response(
1120
- tools=self.tools, response=response
1121
- )
1122
- # Add tool responses to message history
1123
- for tool_resp in tool_responses:
1124
- current_messages.append(tool_resp.to_dict())
1319
+ # Use provided end_strategy or default from instance
1320
+ effective_end_strategy = (
1321
+ end_strategy if end_strategy is not None else self.end_strategy
1322
+ )
1323
+ effective_end_tool = end_tool if end_tool is not None else self.end_tool
1324
+
1325
+ # Create working tools list with end_tool if needed
1326
+ working_tools = self.tools.copy()
1327
+ if effective_end_strategy == "tool" and effective_end_tool is not None:
1328
+ end_tool_obj = define_tool(effective_end_tool)
1329
+ # Only add if not already present
1330
+ if not any(tool.name == end_tool_obj.name for tool in working_tools):
1331
+ working_tools.append(end_tool_obj)
1332
+
1333
+ # Get effective context settings
1334
+ effective_context_settings = self._get_effective_context_settings(
1335
+ context_updates=context_updates,
1336
+ context_confirm=context_confirm,
1337
+ context_strategy=context_strategy,
1338
+ context_max_retries=context_max_retries,
1339
+ context_confirm_instructions=context_confirm_instructions,
1340
+ context_selection_instructions=context_selection_instructions,
1341
+ context_update_instructions=context_update_instructions,
1342
+ context_format=context_format,
1343
+ )
1125
1344
 
1126
- # This is not the final step, add to steps
1127
- steps.append(response)
1128
- else:
1129
- # No tool calls - this is the final step
1130
- # Update context after processing if configured
1345
+ # Parse initial messages
1346
+ parsed_messages = parse_messages(messages)
1347
+ current_messages = parsed_messages.copy()
1348
+ steps: List[LanguageModelResponse[str]] = []
1349
+
1350
+ # RUN MAIN AGENTIC LOOP
1351
+ for step in range(max_steps):
1352
+ # Update context before processing if configured
1131
1353
  if context and self._should_update_context(
1132
- context, "after", effective_context_settings["context_updates"]
1354
+ context, "before", effective_context_settings["context_updates"]
1133
1355
  ):
1134
1356
  context = self._perform_context_update(
1135
1357
  context=context,
1136
1358
  model=working_model,
1137
1359
  current_messages=current_messages,
1138
- timing="after",
1360
+ timing="before",
1139
1361
  effective_settings=effective_context_settings,
1140
1362
  )
1141
- return _create_agent_response_from_language_model_response(
1142
- response=response, steps=steps, context=context
1363
+
1364
+ # Format messages with instructions and context for first step only
1365
+ if step == 0:
1366
+ formatted_messages = self._format_messages_with_context(
1367
+ messages=current_messages,
1368
+ context=context,
1369
+ )
1370
+ else:
1371
+ formatted_messages = current_messages
1372
+
1373
+ # Prepare kwargs for language model
1374
+ model_kwargs = kwargs.copy()
1375
+ # Don't add output_type for intermediate steps - only for final response
1376
+ if self.instructor_mode:
1377
+ model_kwargs["instructor_mode"] = self.instructor_mode
1378
+
1379
+ # Get language model response
1380
+ response = await working_model.async_run(
1381
+ messages=formatted_messages,
1382
+ tools=[tool.to_dict() for tool in working_tools]
1383
+ if working_tools
1384
+ else None,
1385
+ **model_kwargs,
1143
1386
  )
1144
1387
 
1145
- # Max steps reached - return last response
1146
- if steps:
1147
- final_response = steps[-1]
1148
- else:
1149
- # No steps taken, make a final call
1150
- final_response = await working_model.async_run(
1151
- messages=self._format_messages_with_context(
1152
- messages=current_messages,
1388
+ # Check if response has tool calls
1389
+ if response.has_tool_calls():
1390
+ # Add response to message history (with tool calls)
1391
+ current_messages.append(response.to_message())
1392
+
1393
+ # Execute tools and add their responses to messages
1394
+ tool_responses = execute_tools_from_language_model_response(
1395
+ tools=working_tools, response=response
1396
+ )
1397
+ # Add tool responses to message history
1398
+ for tool_resp in tool_responses:
1399
+ current_messages.append(tool_resp.to_dict())
1400
+
1401
+ # This is not the final step, add to steps
1402
+ steps.append(response)
1403
+ else:
1404
+ # No tool calls - check if this is actually the final step based on end_strategy
1405
+ if effective_end_strategy == "tool":
1406
+ # Check if the end_tool was called
1407
+ end_tool_called = (
1408
+ any(
1409
+ tool_call.function.name == effective_end_tool.__name__
1410
+ for tool_call in response.tool_calls
1411
+ )
1412
+ if response.tool_calls
1413
+ else False
1414
+ )
1415
+
1416
+ if not end_tool_called:
1417
+ # End tool not called, continue the conversation
1418
+ logger.debug(
1419
+ f"Agent '{self.name}' step {step + 1}: No end tool called, continuing..."
1420
+ )
1421
+
1422
+ # Add the response to history
1423
+ current_messages.append(response.to_message())
1424
+
1425
+ # Add system message instructing agent to call the end tool
1426
+ current_messages.append(
1427
+ {
1428
+ "role": "system",
1429
+ "content": f"You must call the {effective_end_tool.__name__} tool to complete your response. Do not provide a final answer until you have called this tool.",
1430
+ }
1431
+ )
1432
+
1433
+ # Add user message to continue
1434
+ current_messages.append(
1435
+ {"role": "user", "content": "continue"}
1436
+ )
1437
+
1438
+ # Remove the continue message and append assistant content
1439
+ current_messages.pop() # Remove "continue" message
1440
+
1441
+ # This is not the final step, add to steps and continue
1442
+ steps.append(response)
1443
+ continue
1444
+
1445
+ # This is the final step (either no end_strategy or end_tool was called)
1446
+ # Now we can make the final call with the output_type if specified
1447
+ if output_type:
1448
+ # Make a final call with the structured output type
1449
+ final_model_kwargs = kwargs.copy()
1450
+ final_model_kwargs["type"] = output_type
1451
+ if self.instructor_mode:
1452
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1453
+
1454
+ # Create a clean conversation history for structured output
1455
+ # Include the original messages and the final response content
1456
+ clean_messages = []
1457
+ # Add original user messages (excluding tool calls/responses)
1458
+ for msg in formatted_messages:
1459
+ if isinstance(msg, dict) and msg.get("role") not in [
1460
+ "tool",
1461
+ "assistant",
1462
+ ]:
1463
+ clean_messages.append(msg)
1464
+ elif hasattr(msg, "role") and msg.role not in [
1465
+ "tool",
1466
+ "assistant",
1467
+ ]:
1468
+ clean_messages.append(msg.to_dict())
1469
+
1470
+ # Add the final assistant response content
1471
+ clean_messages.append(
1472
+ {"role": "assistant", "content": response.get_content()}
1473
+ )
1474
+
1475
+ # Use the clean conversation history to generate structured output
1476
+ final_response = await working_model.async_run(
1477
+ messages=clean_messages,
1478
+ **final_model_kwargs,
1479
+ )
1480
+
1481
+ # Update context after processing if configured
1482
+ if context and self._should_update_context(
1483
+ context,
1484
+ "after",
1485
+ effective_context_settings["context_updates"],
1486
+ ):
1487
+ context = self._perform_context_update(
1488
+ context=context,
1489
+ model=working_model,
1490
+ current_messages=current_messages,
1491
+ timing="after",
1492
+ effective_settings=effective_context_settings,
1493
+ )
1494
+ return _create_agent_response_from_language_model_response(
1495
+ response=final_response, steps=steps, context=context
1496
+ )
1497
+ else:
1498
+ # Update context after processing if configured
1499
+ if context and self._should_update_context(
1500
+ context,
1501
+ "after",
1502
+ effective_context_settings["context_updates"],
1503
+ ):
1504
+ context = self._perform_context_update(
1505
+ context=context,
1506
+ model=working_model,
1507
+ current_messages=current_messages,
1508
+ timing="after",
1509
+ effective_settings=effective_context_settings,
1510
+ )
1511
+ return _create_agent_response_from_language_model_response(
1512
+ response=response, steps=steps, context=context
1513
+ )
1514
+
1515
+ # Max steps reached - return last response
1516
+ if steps:
1517
+ final_response = steps[-1]
1518
+ # If we have an output_type, make a final structured call
1519
+ if output_type:
1520
+ final_model_kwargs = kwargs.copy()
1521
+ final_model_kwargs["type"] = output_type
1522
+ if self.instructor_mode:
1523
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1524
+
1525
+ # Create clean messages for structured output
1526
+ clean_messages = []
1527
+ formatted_messages = self._format_messages_with_context(
1528
+ messages=current_messages,
1529
+ context=context,
1530
+ )
1531
+
1532
+ # Add original user messages (excluding tool calls/responses)
1533
+ for msg in formatted_messages:
1534
+ if isinstance(msg, dict) and msg.get("role") not in [
1535
+ "tool",
1536
+ "assistant",
1537
+ ]:
1538
+ clean_messages.append(msg)
1539
+ elif hasattr(msg, "role") and msg.role not in [
1540
+ "tool",
1541
+ "assistant",
1542
+ ]:
1543
+ clean_messages.append(msg.to_dict())
1544
+
1545
+ # Add final response content
1546
+ clean_messages.append(
1547
+ {"role": "assistant", "content": final_response.get_content()}
1548
+ )
1549
+
1550
+ final_response = await working_model.async_run(
1551
+ messages=clean_messages,
1552
+ **final_model_kwargs,
1553
+ )
1554
+ else:
1555
+ # No steps taken, make a final call
1556
+ final_model_kwargs = kwargs.copy()
1557
+ if output_type:
1558
+ final_model_kwargs["type"] = output_type
1559
+ if self.instructor_mode:
1560
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1561
+
1562
+ final_response = await working_model.async_run(
1563
+ messages=self._format_messages_with_context(
1564
+ messages=current_messages,
1565
+ context=context,
1566
+ ),
1567
+ **final_model_kwargs,
1568
+ )
1569
+
1570
+ # Update context after processing if configured
1571
+ if context and self._should_update_context(
1572
+ context, "after", effective_context_settings["context_updates"]
1573
+ ):
1574
+ context = self._perform_context_update(
1153
1575
  context=context,
1154
- ),
1155
- **model_kwargs,
1156
- )
1576
+ model=working_model,
1577
+ current_messages=current_messages,
1578
+ timing="after",
1579
+ effective_settings=effective_context_settings,
1580
+ )
1157
1581
 
1158
- # Update context after processing if configured
1159
- if context and self._should_update_context(
1160
- context, "after", effective_context_settings["context_updates"]
1161
- ):
1162
- context = self._perform_context_update(
1163
- context=context,
1164
- model=working_model,
1165
- current_messages=current_messages,
1166
- timing="after",
1167
- effective_settings=effective_context_settings,
1582
+ return _create_agent_response_from_language_model_response(
1583
+ response=final_response, steps=steps, context=context
1168
1584
  )
1169
1585
 
1170
- return _create_agent_response_from_language_model_response(
1171
- response=final_response, steps=steps, context=context
1172
- )
1586
+ finally:
1587
+ # Restore original logger level
1588
+ if debug is not None or verbose is not None:
1589
+ logger.level = original_level
1173
1590
 
1174
1591
  def stream(
1175
1592
  self,
@@ -1221,6 +1638,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
1221
1638
  context_selection_instructions: Optional[str] = None,
1222
1639
  context_update_instructions: Optional[str] = None,
1223
1640
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
1641
+ end_strategy: Optional[Literal["tool"]] = None,
1642
+ end_tool: Optional[Callable] = None,
1224
1643
  **kwargs: Any,
1225
1644
  ) -> AgentStream[T, AgentContext]:
1226
1645
  """Iterate over agent steps, yielding each step response.
@@ -1314,6 +1733,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
1314
1733
  context=context,
1315
1734
  output_type=output_type,
1316
1735
  stream=True,
1736
+ end_strategy=end_strategy,
1737
+ end_tool=end_tool,
1317
1738
  **kwargs,
1318
1739
  )
1319
1740
 
@@ -1370,6 +1791,8 @@ def create_agent(
1370
1791
  context_selection_instructions: Optional[str] = None,
1371
1792
  context_update_instructions: Optional[str] = None,
1372
1793
  context_format: Literal["json", "python", "markdown"] = "json",
1794
+ verbose: bool = False,
1795
+ debug: bool = False,
1373
1796
  **kwargs: Any,
1374
1797
  ) -> Agent[T]:
1375
1798
  """Create a new AI agent with specified capabilities and behavior.
@@ -1394,6 +1817,8 @@ def create_agent(
1394
1817
  context_selection_instructions: Custom instructions for context selection
1395
1818
  context_update_instructions: Custom instructions for context updates
1396
1819
  context_format: Format for context display - "json", "python", or "markdown"
1820
+ verbose: If True, set logger to INFO level for detailed output
1821
+ debug: If True, set logger to DEBUG level for maximum verbosity
1397
1822
  **kwargs: Additional parameters passed to the underlying language model
1398
1823
 
1399
1824
  Example:
@@ -1425,6 +1850,8 @@ def create_agent(
1425
1850
  context_selection_instructions=context_selection_instructions,
1426
1851
  context_update_instructions=context_update_instructions,
1427
1852
  context_format=context_format,
1853
+ verbose=verbose,
1854
+ debug=debug,
1428
1855
  **kwargs,
1429
1856
  )
1430
1857