agent-framework-openai 1.2.2__tar.gz → 1.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-openai
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: OpenAI integrations for Microsoft Agent Framework.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Typing :: Typed
18
18
  License-File: LICENSE
19
- Requires-Dist: agent-framework-core>=1.2.2,<2
19
+ Requires-Dist: agent-framework-core>=1.3.0,<2
20
20
  Requires-Dist: openai>=1.99.0,<3
21
21
  Project-URL: homepage, https://aka.ms/agent-framework
22
22
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
@@ -121,6 +121,14 @@ OPENAI_LOCAL_SHELL_COMMAND_PARTS_KEY = "openai.local_shell_command_parts"
121
121
  OPENAI_SHELL_OUTPUT_TYPE_SHELL_CALL = "shell_call_output"
122
122
  OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL = "local_shell_call_output"
123
123
 
124
+ # Internal marker emitted by `_prepare_content_for_openai` for an
125
+ # `mcp_server_tool_result` Content. The Responses API expects an `mcp_call`
126
+ # input item to carry both arguments and output as one item, so result
127
+ # Contents cannot be serialized standalone. `_prepare_messages_for_openai`
128
+ # coalesces these markers into the most recent matching `mcp_call` input
129
+ # item before returning, dropping any that are unmatched.
130
+ _AF_MCP_PENDING_OUTPUT_KEY = "__af_pending_mcp_result__"
131
+
124
132
 
125
133
  class OpenAIContinuationToken(ContinuationToken):
126
134
  """Continuation token for OpenAI Responses API background operations."""
@@ -196,6 +204,11 @@ class OpenAIChatOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT],
196
204
  """Configuration for reasoning models (gpt-5, o-series).
197
205
  See: https://platform.openai.com/docs/guides/reasoning"""
198
206
 
207
+ verbosity: Literal["low", "medium", "high"]
208
+ """Output verbosity for GPT-5 family models. Lower values yield shorter responses.
209
+ Translated to ``text.verbosity`` when sent to the Responses API.
210
+ See: https://developers.openai.com/cookbook/examples/gpt-5/gpt-5_new_params_and_tools#1-verbosity-parameter"""
211
+
199
212
  safety_identifier: str
200
213
  """A stable identifier for detecting policy violations.
201
214
  Recommend hashing username/email to avoid sending identifying info."""
@@ -654,7 +667,16 @@ class RawOpenAIChatClient( # type: ignore[misc]
654
667
  response = await client.responses.retrieve(continuation_token["response_id"])
655
668
  except Exception as ex:
656
669
  self._handle_request_error(ex)
657
- return self._parse_response_from_openai(response, options=validated_options)
670
+ chat_response = self._parse_response_from_openai(response, options=validated_options)
671
+ # Once the background response completes, drop the continuation_token from
672
+ # the caller's options dict. FunctionInvocationLayer reuses the same dict
673
+ # across tool-loop iterations, so leaving it in place makes the next iteration
674
+ # retrieve the same completed response again instead of POSTing tool results
675
+ # (issue #5394). Keep `background` so subsequent iterations still create
676
+ # background responses.
677
+ if chat_response.continuation_token is None and isinstance(options, dict):
678
+ options.pop("continuation_token", None)
679
+ return chat_response
658
680
  client, run_options, validated_options = await self._prepare_request(messages, options)
659
681
  try:
660
682
  if "text_format" in run_options:
@@ -1296,6 +1318,12 @@ class RawOpenAIChatClient( # type: ignore[misc]
1296
1318
  "type": "function",
1297
1319
  "name": func_name,
1298
1320
  }
1321
+ elif mode == "auto" and (allowed := tool_mode.get("allowed_tools")) is not None:
1322
+ run_options["tool_choice"] = {
1323
+ "type": "allowed_tools",
1324
+ "mode": "auto",
1325
+ "tools": [{"type": "function", "name": name} for name in allowed],
1326
+ }
1299
1327
  else:
1300
1328
  run_options["tool_choice"] = mode
1301
1329
  else:
@@ -1308,6 +1336,11 @@ class RawOpenAIChatClient( # type: ignore[misc]
1308
1336
  response_format, text_config = self._prepare_response_and_text_format(
1309
1337
  response_format=response_format, text_config=text_config
1310
1338
  )
1339
+ # The Responses API nests verbosity under ``text.verbosity``; surface it as a
1340
+ # top-level option for parity with ``reasoning`` and translate here.
1341
+ if (verbosity := run_options.pop("verbosity", None)) is not None:
1342
+ text_config = dict(text_config) if text_config else {}
1343
+ text_config["verbosity"] = verbosity
1311
1344
  if text_config:
1312
1345
  run_options["text"] = text_config
1313
1346
  if response_format:
@@ -1357,7 +1390,10 @@ class RawOpenAIChatClient( # type: ignore[misc]
1357
1390
  for message in chat_messages
1358
1391
  ]
1359
1392
  # Flatten the list of lists into a single list
1360
- return list(chain.from_iterable(list_of_list))
1393
+ flat = list(chain.from_iterable(list_of_list))
1394
+ # Coalesce hosted-MCP result markers onto matching mcp_call input
1395
+ # items (drop unmatched). See `_AF_MCP_PENDING_OUTPUT_KEY`.
1396
+ return self._coalesce_pending_mcp_results(flat)
1361
1397
 
1362
1398
  def _prepare_message_for_openai(
1363
1399
  self,
@@ -1422,6 +1458,18 @@ class RawOpenAIChatClient( # type: ignore[misc]
1422
1458
  )
1423
1459
  if prepared:
1424
1460
  all_messages.append(prepared)
1461
+ case "mcp_server_tool_call" | "mcp_server_tool_result":
1462
+ # Hosted MCP call/result contents serialize as a single
1463
+ # top-level mcp_call input item; the result side emits an
1464
+ # internal marker that `_prepare_messages_for_openai`
1465
+ # coalesces onto the matching call (or drops if unmatched).
1466
+ prepared_mcp = self._prepare_content_for_openai(
1467
+ message.role,
1468
+ content,
1469
+ replays_local_storage=replays_local_storage,
1470
+ )
1471
+ if prepared_mcp:
1472
+ all_messages.append(prepared_mcp)
1425
1473
  case _:
1426
1474
  prepared_content = self._prepare_content_for_openai(
1427
1475
  message.role,
@@ -1600,6 +1648,24 @@ class RawOpenAIChatClient( # type: ignore[misc]
1600
1648
  "approval_request_id": content.id,
1601
1649
  "approve": content.approved,
1602
1650
  }
1651
+ case "mcp_server_tool_call":
1652
+ if not content.call_id:
1653
+ return {}
1654
+ return {
1655
+ "type": "mcp_call",
1656
+ "id": content.call_id,
1657
+ "server_label": content.server_name or "",
1658
+ "name": content.tool_name or "",
1659
+ "arguments": self._stringify_mcp_arguments(content.arguments),
1660
+ }
1661
+ case "mcp_server_tool_result":
1662
+ if not content.call_id:
1663
+ return {}
1664
+ return {
1665
+ _AF_MCP_PENDING_OUTPUT_KEY: True,
1666
+ "call_id": content.call_id,
1667
+ "output": self._stringify_mcp_output(content.output),
1668
+ }
1603
1669
  case "hosted_file":
1604
1670
  # `input_file` is an input-only content type in the Responses API and is rejected
1605
1671
  # inside an assistant message. Hosted-file content on an assistant message
@@ -1675,6 +1741,91 @@ class RawOpenAIChatClient( # type: ignore[misc]
1675
1741
  """Join shell commands into a single executable command string."""
1676
1742
  return "\n".join(command for command in commands if command).strip()
1677
1743
 
1744
+ @staticmethod
1745
+ def _stringify_mcp_arguments(arguments: Any) -> str:
1746
+ """Render hosted-MCP tool-call arguments as a JSON string for the Responses API."""
1747
+ if arguments is None:
1748
+ return ""
1749
+ if isinstance(arguments, str):
1750
+ return arguments
1751
+ try:
1752
+ return json.dumps(arguments)
1753
+ except (TypeError, ValueError):
1754
+ return str(arguments)
1755
+
1756
+ @staticmethod
1757
+ def _stringify_mcp_output(output: Any) -> str:
1758
+ """Render a hosted-MCP tool-call result into the string `mcp_call.output` field.
1759
+
1760
+ Accepts a string, a list of text-bearing Content objects (the form
1761
+ the chat client produces when parsing an `mcp_call` Responses item),
1762
+ or any other value. List entries that are dicts with the canonical
1763
+ MCP text-content shape (`{"text": "..."}`) are unwrapped to their
1764
+ text. Anything else falls back to JSON encoding rather than Python
1765
+ `repr`, so the wire payload stays parseable for downstream callers.
1766
+ """
1767
+ if output is None:
1768
+ return ""
1769
+ if isinstance(output, str):
1770
+ return output
1771
+ if isinstance(output, Sequence) and not isinstance(output, (str, bytes, bytearray)):
1772
+ # cast is for pyright (reportUnknownVariableType); mypy considers
1773
+ # it redundant after the isinstance narrowing.
1774
+ entries = cast(Sequence[Any], output) # type: ignore[redundant-cast]
1775
+ parts: list[str] = []
1776
+ for entry in entries:
1777
+ if isinstance(entry, str):
1778
+ parts.append(entry)
1779
+ continue
1780
+ text = getattr(entry, "text", None)
1781
+ if isinstance(text, str):
1782
+ parts.append(text)
1783
+ continue
1784
+ if isinstance(entry, Mapping):
1785
+ mapping_text = cast(Any, entry).get("text")
1786
+ if isinstance(mapping_text, str):
1787
+ parts.append(mapping_text)
1788
+ continue
1789
+ parts.append(json.dumps(entry, default=str))
1790
+ return "".join(parts)
1791
+ return json.dumps(output, default=str)
1792
+
1793
+ @staticmethod
1794
+ def _coalesce_pending_mcp_results(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
1795
+ """Merge pending hosted-MCP result markers onto matching mcp_call input items.
1796
+
1797
+ See `_AF_MCP_PENDING_OUTPUT_KEY`. The Responses API expects a single
1798
+ `mcp_call` input item carrying both `arguments` and `output`, so a
1799
+ result Content cannot be its own input item. Any unmatched markers
1800
+ are dropped (debug-logged); surfacing them as standalone items
1801
+ would produce the orphan `function_call_output` / `mcp_call_output`
1802
+ the API rejects.
1803
+ """
1804
+ out: list[dict[str, Any]] = []
1805
+ for item in items:
1806
+ if item.get(_AF_MCP_PENDING_OUTPUT_KEY):
1807
+ target_call_id = item.get("call_id")
1808
+ target = next(
1809
+ (
1810
+ existing
1811
+ for existing in reversed(out)
1812
+ if existing.get("type") == "mcp_call" and existing.get("id") == target_call_id
1813
+ ),
1814
+ None,
1815
+ )
1816
+ if target is not None:
1817
+ if target.get("output") is None:
1818
+ target["output"] = item.get("output")
1819
+ else:
1820
+ logger.debug(
1821
+ "Dropping orphan mcp_server_tool_result for call_id=%s; "
1822
+ "no matching mcp_call appeared in input.",
1823
+ target_call_id,
1824
+ )
1825
+ continue
1826
+ out.append(item)
1827
+ return out
1828
+
1678
1829
  @staticmethod
1679
1830
  def _serialize_provider_payload(value: Any) -> Any:
1680
1831
  """Convert OpenAI SDK objects into JSON-serializable Python values."""
@@ -145,6 +145,9 @@ class OpenAIChatCompletionOptions(ChatOptions[ResponseModelT], Generic[ResponseM
145
145
  logprobs: bool
146
146
  top_logprobs: int
147
147
  prediction: Prediction
148
+ verbosity: Literal["low", "medium", "high"]
149
+ """Output verbosity for GPT-5 family models. Lower values yield shorter responses.
150
+ See: https://developers.openai.com/cookbook/examples/gpt-5/gpt-5_new_params_and_tools#1-verbosity-parameter"""
148
151
 
149
152
 
150
153
  OpenAIChatCompletionOptionsT = TypeVar(
@@ -662,6 +665,12 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
662
665
  "type": "function",
663
666
  "function": {"name": func_name},
664
667
  }
668
+ elif mode in ("auto", "required") and tool_mode.get("allowed_tools") is not None:
669
+ logger.warning(
670
+ "allowed_tools is not supported by the Chat Completions API; "
671
+ "the setting will be ignored. Use OpenAIChatClient (Responses API) instead."
672
+ )
673
+ run_options["tool_choice"] = mode
665
674
  else:
666
675
  run_options["tool_choice"] = mode
667
676
 
@@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework."
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- version = "1.2.2"
7
+ version = "1.3.0"
8
8
  license-files = ["LICENSE"]
9
9
  urls.homepage = "https://aka.ms/agent-framework"
10
10
  urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Typing :: Typed",
24
24
  ]
25
25
  dependencies = [
26
- "agent-framework-core>=1.2.2,<2",
26
+ "agent-framework-core>=1.3.0,<2",
27
27
  "openai>=1.99.0,<3",
28
28
  ]
29
29