Skip to content

Commit

Permalink
Merge pull request #1358 from gauravdhiman/supporting_structured_outp…
Browse files Browse the repository at this point in the history
…ut_for_function_calling

Made changes to support structured output from OpenAI models along with tools calling
  • Loading branch information
ashpreetbedi authored Nov 3, 2024
2 parents dc22ca5 + 952264d commit a7361e2
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 5 deletions.
5 changes: 4 additions & 1 deletion phi/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,10 @@ def update_model(self) -> None:
agent_tools = self.get_tools()
if agent_tools is not None:
for tool in agent_tools:
self.model.add_tool(tool)
if self.response_model is not None and self.structured_outputs:
self.model.add_tool(tool, structured_outputs=True)
else:
self.model.add_tool(tool)

# Set show_tool_calls if it is not set on the Model
if self.model.show_tool_calls is None and self.show_tool_calls is not None:
Expand Down
10 changes: 9 additions & 1 deletion phi/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]:
tools_for_api.append(tool)
return tools_for_api

def add_tool(self, tool: Union[Tool, Toolkit, Callable, Dict, Function]) -> None:
def add_tool(self, tool: Union[Tool, Toolkit, Callable, Dict, Function], structured_outputs: bool = False) -> None:
if self.tools is None:
self.tools = []

# If the tool is a Tool or Dict, add it directly to the Model
if isinstance(tool, Tool) or isinstance(tool, Dict):
if structured_outputs:
tool.strict = True
if tool not in self.tools:
self.tools.append(tool)
logger.debug(f"Added tool {tool} to model.")
Expand All @@ -141,13 +143,17 @@ def add_tool(self, tool: Union[Tool, Toolkit, Callable, Dict, Function]) -> None
if isinstance(tool, Toolkit):
# For each function in the toolkit
for name, func in tool.functions.items():
if structured_outputs:
func.strict = True
# If the function does not exist in self.functions, add to self.tools
if name not in self.functions:
self.functions[name] = func
self.tools.append({"type": "function", "function": func.to_dict()})
logger.debug(f"Function {name} from {tool.name} added to model.")

elif isinstance(tool, Function):
if structured_outputs:
tool.strict = True
if tool.name not in self.functions:
self.functions[tool.name] = tool
self.tools.append({"type": "function", "function": tool.to_dict()})
Expand All @@ -158,6 +164,8 @@ def add_tool(self, tool: Union[Tool, Toolkit, Callable, Dict, Function]) -> None
function_name = tool.__name__
if function_name not in self.functions:
func = Function.from_callable(tool)
if structured_outputs:
func.strict = True
self.functions[func.name] = func
self.tools.append({"type": "function", "function": func.to_dict()})
logger.debug(f"Function {func.name} added to Model.")
Expand Down
4 changes: 4 additions & 0 deletions phi/model/openai/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@ def response(self, messages: List[Message]) -> ModelResponse:
if model_response.content is None:
model_response.content = ""
model_response.content += response_after_tool_calls.content
if response_after_tool_calls.parsed is not None:
# bubble up the parsed object, so that the final response has the parsed object
# that is visible to the agent
model_response.parsed = response_after_tool_calls.parsed
return model_response

# -*- Update model response
Expand Down
5 changes: 3 additions & 2 deletions phi/tools/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ class Function(BaseModel):
# To describe a function that accepts no parameters, provide the value {"type": "object", "properties": {}}.
parameters: Dict[str, Any] = {"type": "object", "properties": {}}
entrypoint: Optional[Callable] = None
strict: bool = False

# If True, the arguments are sanitized before being passed to the function.
sanitize_arguments: bool = True

def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, include={"name", "description", "parameters"})
return self.model_dump(exclude_none=True, include={"name", "description", "parameters", "strict"})

@classmethod
def from_callable(cls, c: Callable) -> "Function":
from inspect import getdoc
from phi.utils.json_schema import get_json_schema

parameters = {"type": "object", "properties": {}}
parameters = {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
try:
# logger.info(f"Getting type hints for {c}")
type_hints = get_type_hints(c)
Expand Down
3 changes: 2 additions & 1 deletion phi/utils/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ def get_json_schema_for_arg(t: Any) -> Optional[Any]:


def get_json_schema(type_hints: Dict[str, Any]) -> Dict[str, Any]:
json_schema: Dict[str, Any] = {"type": "object", "properties": {}}
json_schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
for k, v in type_hints.items():
# logger.info(f"Parsing arg: {k} | {v}")
if k == "return":
continue
json_schema["required"].append(k)
arg_json_schema = get_json_schema_for_arg(v)
if arg_json_schema is not None:
# logger.info(f"json_schema: {arg_json_schema}")
Expand Down

0 comments on commit a7361e2

Please sign in to comment.