hammad-python 0.0.23__py3-none-any.whl → 0.0.25__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.
Files changed (40) hide show
  1. hammad/__init__.py +62 -14
  2. hammad/_main.py +226 -0
  3. hammad/cli/__init__.py +0 -2
  4. hammad/cli/plugins.py +3 -1
  5. hammad/data/__init__.py +4 -5
  6. hammad/data/types/__init__.py +37 -1
  7. hammad/data/types/file.py +74 -1
  8. hammad/data/types/multimodal/__init__.py +14 -2
  9. hammad/data/types/multimodal/audio.py +106 -2
  10. hammad/data/types/multimodal/image.py +104 -2
  11. hammad/data/types/text.py +242 -0
  12. hammad/genai/__init__.py +73 -0
  13. hammad/genai/a2a/__init__.py +32 -0
  14. hammad/genai/a2a/workers.py +552 -0
  15. hammad/genai/agents/__init__.py +8 -0
  16. hammad/genai/agents/agent.py +747 -214
  17. hammad/genai/agents/run.py +421 -12
  18. hammad/genai/agents/types/agent_response.py +2 -1
  19. hammad/genai/graphs/__init__.py +125 -0
  20. hammad/genai/graphs/base.py +1786 -0
  21. hammad/genai/graphs/plugins.py +316 -0
  22. hammad/genai/graphs/types.py +638 -0
  23. hammad/genai/models/language/__init__.py +6 -1
  24. hammad/genai/models/language/model.py +46 -0
  25. hammad/genai/models/language/run.py +330 -4
  26. hammad/genai/models/language/types/language_model_response.py +1 -1
  27. hammad/genai/types/tools.py +1 -1
  28. hammad/logging/logger.py +60 -5
  29. hammad/mcp/__init__.py +3 -0
  30. hammad/types.py +288 -0
  31. {hammad_python-0.0.23.dist-info → hammad_python-0.0.25.dist-info}/METADATA +6 -1
  32. {hammad_python-0.0.23.dist-info → hammad_python-0.0.25.dist-info}/RECORD +34 -32
  33. hammad/_main/__init__.py +0 -4
  34. hammad/_main/_fn.py +0 -20
  35. hammad/_main/_new.py +0 -52
  36. hammad/_main/_run.py +0 -50
  37. hammad/_main/_to.py +0 -19
  38. hammad/cli/_runner.py +0 -265
  39. {hammad_python-0.0.23.dist-info → hammad_python-0.0.25.dist-info}/WHEEL +0 -0
  40. {hammad_python-0.0.23.dist-info → hammad_python-0.0.25.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,7 @@ from typing import (
11
11
  Optional,
12
12
  Union,
13
13
  Dict,
14
+ TypeAlias,
14
15
  overload,
15
16
  TYPE_CHECKING,
16
17
  )
@@ -49,7 +50,10 @@ from .types.agent_hooks import HookManager, HookDecorator
49
50
  from .types.agent_messages import AgentMessages
50
51
 
51
52
  if TYPE_CHECKING:
52
- pass
53
+ try:
54
+ from fasta2a import FastA2A
55
+ except ImportError:
56
+ FastA2A: TypeAlias = Any
53
57
 
54
58
 
55
59
  T = TypeVar("T")
@@ -210,6 +214,13 @@ def _update_context_object(
210
214
  raise ValueError(f"Cannot update context of type {type(context)}")
211
215
 
212
216
 
217
+ def mark_complete() -> None:
218
+ """If you feel you are ready to respond to the user, or have completed
219
+ the task given to you, call this function to mark your response as
220
+ complete."""
221
+ return "complete"
222
+
223
+
213
224
  class Agent(BaseGenAIModel, Generic[T]):
214
225
  """A generative AI agent that can execute tools, generate structured outputs,
215
226
  and maintain context across multiple conversation steps.
@@ -245,6 +256,11 @@ class Agent(BaseGenAIModel, Generic[T]):
245
256
  tools: Union[List[Tool], Callable, None] = None,
246
257
  settings: Optional[AgentSettings] = None,
247
258
  instructor_mode: Optional[LanguageModelInstructorMode] = None,
259
+ # Defaults
260
+ max_steps: int = 10,
261
+ # End Strategy
262
+ end_strategy: Literal["tool"] | None = None,
263
+ end_tool: Callable = mark_complete,
248
264
  # Context management parameters
249
265
  context_updates: Optional[
250
266
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
@@ -256,6 +272,8 @@ class Agent(BaseGenAIModel, Generic[T]):
256
272
  context_selection_instructions: Optional[str] = None,
257
273
  context_update_instructions: Optional[str] = None,
258
274
  context_format: Literal["json", "python", "markdown"] = "json",
275
+ verbose: bool = False,
276
+ debug: bool = False,
259
277
  **kwargs: Any,
260
278
  ):
261
279
  """Create a new AI agent with specified capabilities and behavior.
@@ -272,6 +290,11 @@ class Agent(BaseGenAIModel, Generic[T]):
272
290
  tools: List of tools/functions the agent can call, or a single callable
273
291
  settings: AgentSettings object to customize default behavior
274
292
  instructor_mode: Mode for structured output generation
293
+ max_steps: Default ,aximum number of steps the agent can take before stopping
294
+ end_strategy: Optional alternative strategy to provide an end tool for determining agent's final
295
+ response.
296
+ end_tool: The tool the agent will call to determine if it should stop.
297
+ This is only used if end_strategy is set to "tool".
275
298
  context_updates: When to update context - "before", "after", or both
276
299
  context_confirm: Whether to confirm context updates with the user
277
300
  context_strategy: How to select context updates - "selective" or "all"
@@ -280,6 +303,8 @@ class Agent(BaseGenAIModel, Generic[T]):
280
303
  context_selection_instructions: Custom instructions for context selection
281
304
  context_update_instructions: Custom instructions for context updates
282
305
  context_format: Format for context display - "json", "python", or "markdown"
306
+ verbose: If True, set logger to INFO level for detailed output
307
+ debug: If True, set logger to DEBUG level for maximum verbosity
283
308
  **kwargs: Additional parameters passed to the underlying language model
284
309
 
285
310
  Example:
@@ -307,6 +332,17 @@ class Agent(BaseGenAIModel, Generic[T]):
307
332
  self.settings = settings or AgentSettings()
308
333
  self.instructor_mode = instructor_mode
309
334
 
335
+ # Store max_steps as instance variable (overrides settings if provided)
336
+ self.max_steps = max_steps if max_steps is not None else self.settings.max_steps
337
+
338
+ # Store end strategy parameters
339
+ self.end_strategy = end_strategy
340
+ self.end_tool = end_tool if end_tool is not None else mark_complete
341
+
342
+ # Add end_tool to tools if end_strategy is 'tool'
343
+ if self.end_strategy == "tool":
344
+ self.tools.append(define_tool(self.end_tool))
345
+
310
346
  # Process instructions
311
347
  self.instructions = _get_instructions(
312
348
  name=name,
@@ -314,11 +350,23 @@ class Agent(BaseGenAIModel, Generic[T]):
314
350
  add_name_to_instructions=self.settings.add_name_to_instructions,
315
351
  )
316
352
 
353
+ # Store verbose/debug settings
354
+ self.verbose = verbose
355
+ self.debug = debug
356
+
357
+ # Set logger level based on verbose/debug flags
358
+ if debug:
359
+ logger.level = "debug"
360
+ elif verbose:
361
+ logger.level = "info"
362
+
317
363
  # Initialize the language model
318
364
  if isinstance(model, LanguageModel):
319
365
  self._language_model = model
320
366
  else:
321
- self._language_model = LanguageModel(model=model, **kwargs)
367
+ self._language_model = LanguageModel(
368
+ model=model, verbose=verbose, debug=debug, **kwargs
369
+ )
322
370
 
323
371
  # Context management settings
324
372
  self.context_updates = context_updates
@@ -689,6 +737,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
689
737
  max_steps: Optional[int] = None,
690
738
  context: Optional[AgentContext] = None,
691
739
  output_type: Optional[Type[T]] = None,
740
+ end_strategy: Optional[Literal["tool"]] = None,
741
+ end_tool: Optional[Callable] = None,
692
742
  context_updates: Optional[
693
743
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
694
744
  ] = None,
@@ -699,6 +749,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
699
749
  context_selection_instructions: Optional[str] = None,
700
750
  context_update_instructions: Optional[str] = None,
701
751
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
752
+ verbose: Optional[bool] = None,
753
+ debug: Optional[bool] = None,
702
754
  *,
703
755
  stream: Literal[False] = False,
704
756
  **kwargs: Any,
@@ -712,6 +764,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
712
764
  max_steps: Optional[int] = None,
713
765
  context: Optional[AgentContext] = None,
714
766
  output_type: Optional[Type[T]] = None,
767
+ end_strategy: Optional[Literal["tool"]] = None,
768
+ end_tool: Optional[Callable] = None,
715
769
  context_updates: Optional[
716
770
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
717
771
  ] = None,
@@ -722,6 +776,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
722
776
  context_selection_instructions: Optional[str] = None,
723
777
  context_update_instructions: Optional[str] = None,
724
778
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
779
+ verbose: Optional[bool] = None,
780
+ debug: Optional[bool] = None,
725
781
  *,
726
782
  stream: Literal[True],
727
783
  **kwargs: Any,
@@ -734,6 +790,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
734
790
  max_steps: Optional[int] = None,
735
791
  context: Optional[AgentContext] = None,
736
792
  output_type: Optional[Type[T]] = None,
793
+ end_strategy: Optional[Literal["tool"]] = None,
794
+ end_tool: Optional[Callable] = None,
737
795
  context_updates: Optional[
738
796
  Union[List[Literal["before", "after"]], Literal["before", "after"]]
739
797
  ] = None,
@@ -744,6 +802,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
744
802
  context_selection_instructions: Optional[str] = None,
745
803
  context_update_instructions: Optional[str] = None,
746
804
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
805
+ verbose: Optional[bool] = None,
806
+ debug: Optional[bool] = None,
747
807
  stream: bool = False,
748
808
  **kwargs: Any,
749
809
  ) -> Union[AgentResponse[T, AgentContext], AgentStream[T, AgentContext]]:
@@ -831,132 +891,321 @@ Please update the appropriate fields based on the conversation. Only update fiel
831
891
  **kwargs,
832
892
  )
833
893
 
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,
894
+ # Set logger level for this request if specified
895
+ original_level = logger.level
896
+ if debug or (debug is None and self.debug):
897
+ logger.level = "debug"
898
+ elif verbose or (verbose is None and self.verbose):
899
+ logger.level = "info"
900
+
901
+ # Log agent execution start
902
+ logger.info(f"Starting agent '{self.name}' execution")
903
+ logger.debug(
904
+ f"Agent settings: max_steps={max_steps or self.max_steps}, tools={len(self.tools)}"
856
905
  )
857
906
 
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,
907
+ try:
908
+ # Use provided model or default
909
+ if model is None:
910
+ working_model = self.language_model
911
+ elif isinstance(model, str):
912
+ working_model = LanguageModel(
913
+ model=model,
914
+ verbose=verbose or self.verbose,
915
+ debug=debug or self.debug,
882
916
  )
883
917
  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
- )
918
+ working_model = model
899
919
 
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())
920
+ # Use provided max_steps or default from instance
921
+ if max_steps is None:
922
+ max_steps = self.max_steps
904
923
 
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())
924
+ # Use provided end_strategy or default from instance
925
+ effective_end_strategy = (
926
+ end_strategy if end_strategy is not None else self.end_strategy
927
+ )
928
+ effective_end_tool = end_tool if end_tool is not None else self.end_tool
929
+
930
+ # Create working tools list with end_tool if needed
931
+ working_tools = self.tools.copy()
932
+ if effective_end_strategy == "tool" and effective_end_tool is not None:
933
+ end_tool_obj = define_tool(effective_end_tool)
934
+ # Only add if not already present
935
+ if not any(tool.name == end_tool_obj.name for tool in working_tools):
936
+ working_tools.append(end_tool_obj)
937
+
938
+ # Get effective context settings
939
+ effective_context_settings = self._get_effective_context_settings(
940
+ context_updates=context_updates,
941
+ context_confirm=context_confirm,
942
+ context_strategy=context_strategy,
943
+ context_max_retries=context_max_retries,
944
+ context_confirm_instructions=context_confirm_instructions,
945
+ context_selection_instructions=context_selection_instructions,
946
+ context_update_instructions=context_update_instructions,
947
+ context_format=context_format,
948
+ )
912
949
 
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
950
+ # Parse initial messages
951
+ parsed_messages = parse_messages(messages)
952
+ current_messages = parsed_messages.copy()
953
+ steps: List[LanguageModelResponse[str]] = []
954
+
955
+ # RUN MAIN AGENTIC LOOP
956
+ logger.debug(f"Starting agentic loop with max_steps={max_steps}")
957
+ for step in range(max_steps):
958
+ logger.debug(f"Agent step {step + 1}/{max_steps}")
959
+ # Update context before processing if configured
918
960
  if context and self._should_update_context(
919
- context, "after", effective_context_settings["context_updates"]
961
+ context, "before", effective_context_settings["context_updates"]
920
962
  ):
921
963
  context = self._perform_context_update(
922
964
  context=context,
923
965
  model=working_model,
924
966
  current_messages=current_messages,
925
- timing="after",
967
+ timing="before",
926
968
  effective_settings=effective_context_settings,
927
969
  )
928
- return _create_agent_response_from_language_model_response(
929
- response=response, steps=steps, context=context
970
+
971
+ # Format messages with instructions and context for first step only
972
+ if step == 0:
973
+ formatted_messages = self._format_messages_with_context(
974
+ messages=current_messages,
975
+ context=context,
976
+ )
977
+ else:
978
+ formatted_messages = current_messages
979
+
980
+ # Prepare kwargs for language model
981
+ model_kwargs = kwargs.copy()
982
+ # Don't add output_type for intermediate steps - only for final response
983
+ if self.instructor_mode:
984
+ model_kwargs["instructor_mode"] = self.instructor_mode
985
+
986
+ # Get language model response
987
+ response = working_model.run(
988
+ messages=formatted_messages,
989
+ tools=[tool.to_dict() for tool in working_tools]
990
+ if working_tools
991
+ else None,
992
+ **model_kwargs,
930
993
  )
931
994
 
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,
995
+ # Check if response has tool calls
996
+ if response.has_tool_calls():
997
+ logger.info(
998
+ f"Agent '{self.name}' making tool calls: {len(response.tool_calls)} tools"
999
+ )
1000
+ for tool_call in response.tool_calls:
1001
+ logger.debug(
1002
+ f"Tool call: {tool_call.function.name}({tool_call.function.arguments})"
1003
+ )
1004
+
1005
+ # Add response to message history (with tool calls)
1006
+ current_messages.append(response.to_message())
1007
+
1008
+ # Execute tools and add their responses to messages
1009
+ tool_responses = execute_tools_from_language_model_response(
1010
+ tools=working_tools, response=response
1011
+ )
1012
+ # Add tool responses to message history
1013
+ for tool_resp in tool_responses:
1014
+ current_messages.append(tool_resp.to_dict())
1015
+
1016
+ # This is not the final step, add to steps
1017
+ steps.append(response)
1018
+ else:
1019
+ # No tool calls - check if this is actually the final step based on end_strategy
1020
+ if effective_end_strategy == "tool":
1021
+ # Check if the end_tool was called
1022
+ end_tool_called = (
1023
+ any(
1024
+ tool_call.function.name == effective_end_tool.__name__
1025
+ for tool_call in response.tool_calls
1026
+ )
1027
+ if response.tool_calls
1028
+ else False
1029
+ )
1030
+
1031
+ if not end_tool_called:
1032
+ # End tool not called, continue the conversation
1033
+ logger.debug(
1034
+ f"Agent '{self.name}' step {step + 1}: No end tool called, continuing..."
1035
+ )
1036
+
1037
+ # Add the response to history
1038
+ current_messages.append(response.to_message())
1039
+
1040
+ # Add system message instructing agent to call the end tool
1041
+ current_messages.append(
1042
+ {
1043
+ "role": "system",
1044
+ "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.",
1045
+ }
1046
+ )
1047
+
1048
+ # Add user message to continue
1049
+ current_messages.append(
1050
+ {"role": "user", "content": "continue"}
1051
+ )
1052
+
1053
+ # Remove the continue message and append assistant content
1054
+ current_messages.pop() # Remove "continue" message
1055
+
1056
+ # This is not the final step, add to steps and continue
1057
+ steps.append(response)
1058
+ continue
1059
+
1060
+ # This is the final step (either no end_strategy or end_tool was called)
1061
+ logger.info(
1062
+ f"Agent '{self.name}' completed execution in {step + 1} steps"
1063
+ )
1064
+ # Now we can make the final call with the output_type if specified
1065
+ # Only make structured output call for non-str types
1066
+ if output_type and output_type != str:
1067
+ # Make a final call with the structured output type
1068
+ final_model_kwargs = kwargs.copy()
1069
+ final_model_kwargs["type"] = output_type
1070
+ if self.instructor_mode:
1071
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1072
+
1073
+ # Create a clean conversation history for structured output
1074
+ # Include the original messages and the final response content
1075
+ clean_messages = []
1076
+ # Add original user messages (excluding tool calls/responses)
1077
+ for msg in formatted_messages:
1078
+ if isinstance(msg, dict) and msg.get("role") not in [
1079
+ "tool",
1080
+ "assistant",
1081
+ ]:
1082
+ clean_messages.append(msg)
1083
+ elif hasattr(msg, "role") and msg.role not in [
1084
+ "tool",
1085
+ "assistant",
1086
+ ]:
1087
+ clean_messages.append(msg.to_dict())
1088
+
1089
+ # Add the final assistant response content
1090
+ clean_messages.append(
1091
+ {"role": "assistant", "content": response.get_content()}
1092
+ )
1093
+
1094
+ # Use the clean conversation history to generate structured output
1095
+ final_response = working_model.run(
1096
+ messages=clean_messages,
1097
+ **final_model_kwargs,
1098
+ )
1099
+
1100
+ # Update context after processing if configured
1101
+ if context and self._should_update_context(
1102
+ context,
1103
+ "after",
1104
+ effective_context_settings["context_updates"],
1105
+ ):
1106
+ context = self._perform_context_update(
1107
+ context=context,
1108
+ model=working_model,
1109
+ current_messages=current_messages,
1110
+ timing="after",
1111
+ effective_settings=effective_context_settings,
1112
+ )
1113
+ return _create_agent_response_from_language_model_response(
1114
+ response=final_response, steps=steps, context=context
1115
+ )
1116
+ else:
1117
+ # Update context after processing if configured
1118
+ if context and self._should_update_context(
1119
+ context,
1120
+ "after",
1121
+ effective_context_settings["context_updates"],
1122
+ ):
1123
+ context = self._perform_context_update(
1124
+ context=context,
1125
+ model=working_model,
1126
+ current_messages=current_messages,
1127
+ timing="after",
1128
+ effective_settings=effective_context_settings,
1129
+ )
1130
+ return _create_agent_response_from_language_model_response(
1131
+ response=response, steps=steps, context=context
1132
+ )
1133
+
1134
+ # Max steps reached - return last response
1135
+ if steps:
1136
+ final_response = steps[-1]
1137
+ # If we have an output_type, make a final structured call (but not for str)
1138
+ if output_type and output_type != str:
1139
+ final_model_kwargs = kwargs.copy()
1140
+ final_model_kwargs["type"] = output_type
1141
+ if self.instructor_mode:
1142
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1143
+
1144
+ # Create clean messages for structured output
1145
+ clean_messages = []
1146
+ formatted_messages = self._format_messages_with_context(
1147
+ messages=current_messages,
1148
+ context=context,
1149
+ )
1150
+
1151
+ # Add original user messages (excluding tool calls/responses)
1152
+ for msg in formatted_messages:
1153
+ if isinstance(msg, dict) and msg.get("role") not in [
1154
+ "tool",
1155
+ "assistant",
1156
+ ]:
1157
+ clean_messages.append(msg)
1158
+ elif hasattr(msg, "role") and msg.role not in [
1159
+ "tool",
1160
+ "assistant",
1161
+ ]:
1162
+ clean_messages.append(msg.to_dict())
1163
+
1164
+ # Add final response content
1165
+ clean_messages.append(
1166
+ {"role": "assistant", "content": final_response.get_content()}
1167
+ )
1168
+
1169
+ final_response = working_model.run(
1170
+ messages=clean_messages,
1171
+ **final_model_kwargs,
1172
+ )
1173
+ else:
1174
+ # No steps taken, make a final call
1175
+ final_model_kwargs = kwargs.copy()
1176
+ if output_type and output_type != str:
1177
+ final_model_kwargs["type"] = output_type
1178
+ if self.instructor_mode:
1179
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1180
+
1181
+ final_response = working_model.run(
1182
+ messages=self._format_messages_with_context(
1183
+ messages=current_messages,
1184
+ context=context,
1185
+ ),
1186
+ **final_model_kwargs,
1187
+ )
1188
+
1189
+ # Update context after processing if configured
1190
+ if context and self._should_update_context(
1191
+ context, "after", effective_context_settings["context_updates"]
1192
+ ):
1193
+ context = self._perform_context_update(
940
1194
  context=context,
941
- ),
942
- **model_kwargs,
943
- )
1195
+ model=working_model,
1196
+ current_messages=current_messages,
1197
+ timing="after",
1198
+ effective_settings=effective_context_settings,
1199
+ )
944
1200
 
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,
1201
+ return _create_agent_response_from_language_model_response(
1202
+ response=final_response, steps=steps, context=context
955
1203
  )
956
1204
 
957
- return _create_agent_response_from_language_model_response(
958
- response=final_response, steps=steps, context=context
959
- )
1205
+ finally:
1206
+ # Restore original logger level
1207
+ if debug is not None or verbose is not None:
1208
+ logger.level = original_level
960
1209
 
961
1210
  async def async_run(
962
1211
  self,
@@ -975,6 +1224,10 @@ Please update the appropriate fields based on the conversation. Only update fiel
975
1224
  context_selection_instructions: Optional[str] = None,
976
1225
  context_update_instructions: Optional[str] = None,
977
1226
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
1227
+ verbose: Optional[bool] = None,
1228
+ debug: Optional[bool] = None,
1229
+ end_strategy: Optional[Literal["tool"]] = None,
1230
+ end_tool: Optional[Callable] = None,
978
1231
  **kwargs: Any,
979
1232
  ) -> AgentResponse[T, AgentContext]:
980
1233
  """Runs this agent asynchronously and returns a final agent response.
@@ -1044,132 +1297,302 @@ Please update the appropriate fields based on the conversation. Only update fiel
1044
1297
  ... )
1045
1298
  ... return response.output
1046
1299
  """
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,
1300
+ # Set logger level for this request if specified
1301
+ original_level = logger.level
1302
+ if debug or (debug is None and self.debug):
1303
+ logger.level = "debug"
1304
+ elif verbose or (verbose is None and self.verbose):
1305
+ logger.level = "info"
1306
+
1307
+ try:
1308
+ # Use provided model or default
1309
+ if model is None:
1310
+ working_model = self.language_model
1311
+ elif isinstance(model, str):
1312
+ working_model = LanguageModel(
1313
+ model=model,
1314
+ verbose=verbose or self.verbose,
1315
+ debug=debug or self.debug,
1095
1316
  )
1096
1317
  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
- )
1318
+ working_model = model
1112
1319
 
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())
1320
+ # Use provided max_steps or default from instance
1321
+ if max_steps is None:
1322
+ max_steps = self.max_steps
1117
1323
 
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())
1324
+ # Use provided end_strategy or default from instance
1325
+ effective_end_strategy = (
1326
+ end_strategy if end_strategy is not None else self.end_strategy
1327
+ )
1328
+ effective_end_tool = end_tool if end_tool is not None else self.end_tool
1329
+
1330
+ # Create working tools list with end_tool if needed
1331
+ working_tools = self.tools.copy()
1332
+ if effective_end_strategy == "tool" and effective_end_tool is not None:
1333
+ end_tool_obj = define_tool(effective_end_tool)
1334
+ # Only add if not already present
1335
+ if not any(tool.name == end_tool_obj.name for tool in working_tools):
1336
+ working_tools.append(end_tool_obj)
1337
+
1338
+ # Get effective context settings
1339
+ effective_context_settings = self._get_effective_context_settings(
1340
+ context_updates=context_updates,
1341
+ context_confirm=context_confirm,
1342
+ context_strategy=context_strategy,
1343
+ context_max_retries=context_max_retries,
1344
+ context_confirm_instructions=context_confirm_instructions,
1345
+ context_selection_instructions=context_selection_instructions,
1346
+ context_update_instructions=context_update_instructions,
1347
+ context_format=context_format,
1348
+ )
1125
1349
 
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
1350
+ # Parse initial messages
1351
+ parsed_messages = parse_messages(messages)
1352
+ current_messages = parsed_messages.copy()
1353
+ steps: List[LanguageModelResponse[str]] = []
1354
+
1355
+ # RUN MAIN AGENTIC LOOP
1356
+ for step in range(max_steps):
1357
+ # Update context before processing if configured
1131
1358
  if context and self._should_update_context(
1132
- context, "after", effective_context_settings["context_updates"]
1359
+ context, "before", effective_context_settings["context_updates"]
1133
1360
  ):
1134
1361
  context = self._perform_context_update(
1135
1362
  context=context,
1136
1363
  model=working_model,
1137
1364
  current_messages=current_messages,
1138
- timing="after",
1365
+ timing="before",
1139
1366
  effective_settings=effective_context_settings,
1140
1367
  )
1141
- return _create_agent_response_from_language_model_response(
1142
- response=response, steps=steps, context=context
1368
+
1369
+ # Format messages with instructions and context for first step only
1370
+ if step == 0:
1371
+ formatted_messages = self._format_messages_with_context(
1372
+ messages=current_messages,
1373
+ context=context,
1374
+ )
1375
+ else:
1376
+ formatted_messages = current_messages
1377
+
1378
+ # Prepare kwargs for language model
1379
+ model_kwargs = kwargs.copy()
1380
+ # Don't add output_type for intermediate steps - only for final response
1381
+ if self.instructor_mode:
1382
+ model_kwargs["instructor_mode"] = self.instructor_mode
1383
+
1384
+ # Get language model response
1385
+ response = await working_model.async_run(
1386
+ messages=formatted_messages,
1387
+ tools=[tool.to_dict() for tool in working_tools]
1388
+ if working_tools
1389
+ else None,
1390
+ **model_kwargs,
1143
1391
  )
1144
1392
 
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,
1393
+ # Check if response has tool calls
1394
+ if response.has_tool_calls():
1395
+ # Add response to message history (with tool calls)
1396
+ current_messages.append(response.to_message())
1397
+
1398
+ # Execute tools and add their responses to messages
1399
+ tool_responses = execute_tools_from_language_model_response(
1400
+ tools=working_tools, response=response
1401
+ )
1402
+ # Add tool responses to message history
1403
+ for tool_resp in tool_responses:
1404
+ current_messages.append(tool_resp.to_dict())
1405
+
1406
+ # This is not the final step, add to steps
1407
+ steps.append(response)
1408
+ else:
1409
+ # No tool calls - check if this is actually the final step based on end_strategy
1410
+ if effective_end_strategy == "tool":
1411
+ # Check if the end_tool was called
1412
+ end_tool_called = (
1413
+ any(
1414
+ tool_call.function.name == effective_end_tool.__name__
1415
+ for tool_call in response.tool_calls
1416
+ )
1417
+ if response.tool_calls
1418
+ else False
1419
+ )
1420
+
1421
+ if not end_tool_called:
1422
+ # End tool not called, continue the conversation
1423
+ logger.debug(
1424
+ f"Agent '{self.name}' step {step + 1}: No end tool called, continuing..."
1425
+ )
1426
+
1427
+ # Add the response to history
1428
+ current_messages.append(response.to_message())
1429
+
1430
+ # Add system message instructing agent to call the end tool
1431
+ current_messages.append(
1432
+ {
1433
+ "role": "system",
1434
+ "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.",
1435
+ }
1436
+ )
1437
+
1438
+ # Add user message to continue
1439
+ current_messages.append(
1440
+ {"role": "user", "content": "continue"}
1441
+ )
1442
+
1443
+ # Remove the continue message and append assistant content
1444
+ current_messages.pop() # Remove "continue" message
1445
+
1446
+ # This is not the final step, add to steps and continue
1447
+ steps.append(response)
1448
+ continue
1449
+
1450
+ # This is the final step (either no end_strategy or end_tool was called)
1451
+ # Now we can make the final call with the output_type if specified
1452
+ # Only make structured output call for non-str types
1453
+ if output_type and output_type != str:
1454
+ # Make a final call with the structured output type
1455
+ final_model_kwargs = kwargs.copy()
1456
+ final_model_kwargs["type"] = output_type
1457
+ if self.instructor_mode:
1458
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1459
+
1460
+ # Create a clean conversation history for structured output
1461
+ # Include the original messages and the final response content
1462
+ clean_messages = []
1463
+ # Add original user messages (excluding tool calls/responses)
1464
+ for msg in formatted_messages:
1465
+ if isinstance(msg, dict) and msg.get("role") not in [
1466
+ "tool",
1467
+ "assistant",
1468
+ ]:
1469
+ clean_messages.append(msg)
1470
+ elif hasattr(msg, "role") and msg.role not in [
1471
+ "tool",
1472
+ "assistant",
1473
+ ]:
1474
+ clean_messages.append(msg.to_dict())
1475
+
1476
+ # Add the final assistant response content
1477
+ clean_messages.append(
1478
+ {"role": "assistant", "content": response.get_content()}
1479
+ )
1480
+
1481
+ # Use the clean conversation history to generate structured output
1482
+ final_response = await working_model.async_run(
1483
+ messages=clean_messages,
1484
+ **final_model_kwargs,
1485
+ )
1486
+
1487
+ # Update context after processing if configured
1488
+ if context and self._should_update_context(
1489
+ context,
1490
+ "after",
1491
+ effective_context_settings["context_updates"],
1492
+ ):
1493
+ context = self._perform_context_update(
1494
+ context=context,
1495
+ model=working_model,
1496
+ current_messages=current_messages,
1497
+ timing="after",
1498
+ effective_settings=effective_context_settings,
1499
+ )
1500
+ return _create_agent_response_from_language_model_response(
1501
+ response=final_response, steps=steps, context=context
1502
+ )
1503
+ else:
1504
+ # Update context after processing if configured
1505
+ if context and self._should_update_context(
1506
+ context,
1507
+ "after",
1508
+ effective_context_settings["context_updates"],
1509
+ ):
1510
+ context = self._perform_context_update(
1511
+ context=context,
1512
+ model=working_model,
1513
+ current_messages=current_messages,
1514
+ timing="after",
1515
+ effective_settings=effective_context_settings,
1516
+ )
1517
+ return _create_agent_response_from_language_model_response(
1518
+ response=response, steps=steps, context=context
1519
+ )
1520
+
1521
+ # Max steps reached - return last response
1522
+ if steps:
1523
+ final_response = steps[-1]
1524
+ # If we have an output_type, make a final structured call (but not for str)
1525
+ if output_type and output_type != str:
1526
+ final_model_kwargs = kwargs.copy()
1527
+ final_model_kwargs["type"] = output_type
1528
+ if self.instructor_mode:
1529
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1530
+
1531
+ # Create clean messages for structured output
1532
+ clean_messages = []
1533
+ formatted_messages = self._format_messages_with_context(
1534
+ messages=current_messages,
1535
+ context=context,
1536
+ )
1537
+
1538
+ # Add original user messages (excluding tool calls/responses)
1539
+ for msg in formatted_messages:
1540
+ if isinstance(msg, dict) and msg.get("role") not in [
1541
+ "tool",
1542
+ "assistant",
1543
+ ]:
1544
+ clean_messages.append(msg)
1545
+ elif hasattr(msg, "role") and msg.role not in [
1546
+ "tool",
1547
+ "assistant",
1548
+ ]:
1549
+ clean_messages.append(msg.to_dict())
1550
+
1551
+ # Add final response content
1552
+ clean_messages.append(
1553
+ {"role": "assistant", "content": final_response.get_content()}
1554
+ )
1555
+
1556
+ final_response = await working_model.async_run(
1557
+ messages=clean_messages,
1558
+ **final_model_kwargs,
1559
+ )
1560
+ else:
1561
+ # No steps taken, make a final call
1562
+ final_model_kwargs = kwargs.copy()
1563
+ if output_type and output_type != str:
1564
+ final_model_kwargs["type"] = output_type
1565
+ if self.instructor_mode:
1566
+ final_model_kwargs["instructor_mode"] = self.instructor_mode
1567
+
1568
+ final_response = await working_model.async_run(
1569
+ messages=self._format_messages_with_context(
1570
+ messages=current_messages,
1571
+ context=context,
1572
+ ),
1573
+ **final_model_kwargs,
1574
+ )
1575
+
1576
+ # Update context after processing if configured
1577
+ if context and self._should_update_context(
1578
+ context, "after", effective_context_settings["context_updates"]
1579
+ ):
1580
+ context = self._perform_context_update(
1153
1581
  context=context,
1154
- ),
1155
- **model_kwargs,
1156
- )
1582
+ model=working_model,
1583
+ current_messages=current_messages,
1584
+ timing="after",
1585
+ effective_settings=effective_context_settings,
1586
+ )
1157
1587
 
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,
1588
+ return _create_agent_response_from_language_model_response(
1589
+ response=final_response, steps=steps, context=context
1168
1590
  )
1169
1591
 
1170
- return _create_agent_response_from_language_model_response(
1171
- response=final_response, steps=steps, context=context
1172
- )
1592
+ finally:
1593
+ # Restore original logger level
1594
+ if debug is not None or verbose is not None:
1595
+ logger.level = original_level
1173
1596
 
1174
1597
  def stream(
1175
1598
  self,
@@ -1204,6 +1627,106 @@ Please update the appropriate fields based on the conversation. Only update fiel
1204
1627
  **kwargs,
1205
1628
  )
1206
1629
 
1630
+ def as_a2a(
1631
+ self,
1632
+ *,
1633
+ # Worker configuration
1634
+ context: Optional[AgentContext] = None,
1635
+ # Storage and broker configuration
1636
+ storage: Optional[Any] = None,
1637
+ broker: Optional[Any] = None,
1638
+ # Server configuration
1639
+ host: str = "0.0.0.0",
1640
+ port: int = 8000,
1641
+ reload: bool = False,
1642
+ workers: int = 1,
1643
+ log_level: str = "info",
1644
+ # A2A configuration
1645
+ name: Optional[str] = None,
1646
+ url: Optional[str] = None,
1647
+ version: str = "1.0.0",
1648
+ description: Optional[str] = None,
1649
+ # Advanced configuration
1650
+ lifespan_timeout: int = 30,
1651
+ **uvicorn_kwargs: Any,
1652
+ ) -> "FastA2A": # type: ignore
1653
+ """
1654
+ Convert this agent to an A2A server application.
1655
+
1656
+ This method creates a FastA2A server that can handle A2A requests
1657
+ for this agent instance. It sets up the necessary Worker, Storage,
1658
+ and Broker components automatically.
1659
+
1660
+ Args:
1661
+ context: Initial context for the agent
1662
+ storage: Custom storage backend (defaults to InMemoryStorage)
1663
+ broker: Custom broker backend (defaults to InMemoryBroker)
1664
+ host: Host to bind the server to
1665
+ port: Port to bind the server to
1666
+ reload: Enable auto-reload for development
1667
+ workers: Number of worker processes
1668
+ log_level: Logging level
1669
+ name: Agent name for the A2A server (defaults to agent's name)
1670
+ url: URL where the agent is hosted
1671
+ version: API version
1672
+ description: API description for the A2A server (defaults to agent's description)
1673
+ lifespan_timeout: Timeout for lifespan events
1674
+ **uvicorn_kwargs: Additional arguments passed to uvicorn
1675
+
1676
+ Returns:
1677
+ FastA2A application instance that can be run with uvicorn
1678
+
1679
+ Examples:
1680
+ Convert agent to A2A server:
1681
+ ```python
1682
+ agent = Agent(
1683
+ name="assistant",
1684
+ instructions="You are a helpful assistant",
1685
+ model="openai/gpt-4o-mini"
1686
+ )
1687
+
1688
+ app = agent.as_a2a(port=8080)
1689
+
1690
+ # Run with uvicorn
1691
+ import uvicorn
1692
+ uvicorn.run(app, host="0.0.0.0", port=8080)
1693
+ ```
1694
+
1695
+ Or use the CLI:
1696
+ ```bash
1697
+ uvicorn mymodule:agent.as_a2a() --reload
1698
+ ```
1699
+
1700
+ With custom configuration:
1701
+ ```python
1702
+ app = agent.as_a2a(
1703
+ name="My Assistant API",
1704
+ description="A helpful AI assistant",
1705
+ host="localhost",
1706
+ port=3000
1707
+ )
1708
+ ```
1709
+ """
1710
+ from ..a2a import as_a2a_app
1711
+
1712
+ return as_a2a_app(
1713
+ self,
1714
+ context=context,
1715
+ storage=storage,
1716
+ broker=broker,
1717
+ host=host,
1718
+ port=port,
1719
+ reload=reload,
1720
+ workers=workers,
1721
+ log_level=log_level,
1722
+ name=name or self.name,
1723
+ url=url,
1724
+ version=version,
1725
+ description=description or self.description,
1726
+ lifespan_timeout=lifespan_timeout,
1727
+ **uvicorn_kwargs,
1728
+ )
1729
+
1207
1730
  def iter(
1208
1731
  self,
1209
1732
  messages: AgentMessages,
@@ -1221,6 +1744,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
1221
1744
  context_selection_instructions: Optional[str] = None,
1222
1745
  context_update_instructions: Optional[str] = None,
1223
1746
  context_format: Optional[Literal["json", "python", "markdown"]] = None,
1747
+ end_strategy: Optional[Literal["tool"]] = None,
1748
+ end_tool: Optional[Callable] = None,
1224
1749
  **kwargs: Any,
1225
1750
  ) -> AgentStream[T, AgentContext]:
1226
1751
  """Iterate over agent steps, yielding each step response.
@@ -1314,6 +1839,8 @@ Please update the appropriate fields based on the conversation. Only update fiel
1314
1839
  context=context,
1315
1840
  output_type=output_type,
1316
1841
  stream=True,
1842
+ end_strategy=end_strategy,
1843
+ end_tool=end_tool,
1317
1844
  **kwargs,
1318
1845
  )
1319
1846
 
@@ -1370,6 +1897,8 @@ def create_agent(
1370
1897
  context_selection_instructions: Optional[str] = None,
1371
1898
  context_update_instructions: Optional[str] = None,
1372
1899
  context_format: Literal["json", "python", "markdown"] = "json",
1900
+ verbose: bool = False,
1901
+ debug: bool = False,
1373
1902
  **kwargs: Any,
1374
1903
  ) -> Agent[T]:
1375
1904
  """Create a new AI agent with specified capabilities and behavior.
@@ -1394,6 +1923,8 @@ def create_agent(
1394
1923
  context_selection_instructions: Custom instructions for context selection
1395
1924
  context_update_instructions: Custom instructions for context updates
1396
1925
  context_format: Format for context display - "json", "python", or "markdown"
1926
+ verbose: If True, set logger to INFO level for detailed output
1927
+ debug: If True, set logger to DEBUG level for maximum verbosity
1397
1928
  **kwargs: Additional parameters passed to the underlying language model
1398
1929
 
1399
1930
  Example:
@@ -1425,6 +1956,8 @@ def create_agent(
1425
1956
  context_selection_instructions=context_selection_instructions,
1426
1957
  context_update_instructions=context_update_instructions,
1427
1958
  context_format=context_format,
1959
+ verbose=verbose,
1960
+ debug=debug,
1428
1961
  **kwargs,
1429
1962
  )
1430
1963