agentforge-openai 0.2.3__tar.gz → 0.2.4__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: agentforge-openai
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: OpenAI LLM + embeddings provider for AgentForge — gpt-4o / o-series + text-embedding-3-*
5
5
  Project-URL: Homepage, https://github.com/Scaffoldic/agentforge-py
6
6
  Project-URL: Repository, https://github.com/Scaffoldic/agentforge-py
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.13
22
- Requires-Dist: agentforge-core~=0.2.3
22
+ Requires-Dist: agentforge-core~=0.2.4
23
23
  Provides-Extra: openai
24
24
  Requires-Dist: openai>=1.50; extra == 'openai'
25
25
  Description-Content-Type: text/markdown
@@ -8,7 +8,7 @@
8
8
 
9
9
  [project]
10
10
  name = "agentforge-openai"
11
- version = "0.2.3"
11
+ version = "0.2.4"
12
12
  description = "OpenAI LLM + embeddings provider for AgentForge — gpt-4o / o-series + text-embedding-3-*"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.13"
@@ -29,7 +29,7 @@ classifiers = [
29
29
  ]
30
30
 
31
31
  dependencies = [
32
- "agentforge-core ~= 0.2.3",
32
+ "agentforge-core ~= 0.2.4",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
@@ -14,10 +14,12 @@ Finish reasons normalised:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import json
17
18
  from collections.abc import AsyncIterator
18
19
  from typing import TYPE_CHECKING, Any
19
20
 
20
21
  from agentforge_core.contracts.llm import LLMClient
22
+ from agentforge_core.contracts.tool import validate_tool_name
21
23
  from agentforge_core.production.exceptions import ModuleError
22
24
  from agentforge_core.resolver import register_provider
23
25
  from agentforge_core.values.messages import (
@@ -298,12 +300,32 @@ def _message_to_openai(message: Message) -> dict[str, Any]:
298
300
  if message.role == "system":
299
301
  # Should be hoisted by the caller; defensive passthrough.
300
302
  return {"role": "system", "content": message.content}
303
+ if message.role == "assistant" and message.tool_calls:
304
+ # Round-trip tool_calls so the subsequent role="tool" message
305
+ # pairs cleanly via tool_call_id (bug-009).
306
+ return {
307
+ "role": "assistant",
308
+ "content": message.content or None,
309
+ "tool_calls": [
310
+ {
311
+ "id": tc.id,
312
+ "type": "function",
313
+ "function": {
314
+ "name": tc.name,
315
+ "arguments": json.dumps(dict(tc.arguments)),
316
+ },
317
+ }
318
+ for tc in message.tool_calls
319
+ ],
320
+ }
301
321
  return {"role": message.role, "content": message.content}
302
322
 
303
323
 
304
324
  def _tools_to_openai(tools: list[ToolSpec] | None) -> list[dict[str, Any]] | None:
305
325
  if not tools:
306
326
  return None
327
+ for t in tools:
328
+ validate_tool_name(t.name)
307
329
  return [
308
330
  {
309
331
  "type": "function",
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+
5
7
  import pytest
6
- from agentforge_core.values.messages import Message, ToolSpec
8
+ from agentforge_core.production.exceptions import ToolNameInvalidError
9
+ from agentforge_core.values.messages import Message, ToolCall, ToolSpec
7
10
  from agentforge_openai import OpenAIClient
8
11
  from agentforge_openai._inmem_runner import FakeOpenAIRunner
9
12
  from agentforge_openai._pricing import chat_cost_usd
@@ -227,6 +230,46 @@ async def test_tool_role_messages_pass_with_tool_call_id(
227
230
  assert sent["content"] == "42"
228
231
 
229
232
 
233
+ @pytest.mark.asyncio
234
+ async def test_assistant_turn_with_tool_calls_emits_openai_tool_calls(
235
+ client: OpenAIClient,
236
+ fake_runner: FakeOpenAIRunner,
237
+ ) -> None:
238
+ """bug-009: an assistant Message carrying framework tool_calls must
239
+ serialise to OpenAI's `tool_calls` array with JSON-encoded arguments,
240
+ so the subsequent role="tool" message pairs cleanly via tool_call_id."""
241
+ fake_runner.set_chat_response(
242
+ {
243
+ "model": "gpt-4o-mini",
244
+ "choices": [
245
+ {
246
+ "index": 0,
247
+ "message": {"role": "assistant", "content": ""},
248
+ "finish_reason": "stop",
249
+ },
250
+ ],
251
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
252
+ },
253
+ )
254
+ await client.call(
255
+ system="",
256
+ messages=[
257
+ Message(
258
+ role="assistant",
259
+ content="calling",
260
+ tool_calls=(ToolCall(id="call_1", name="search", arguments={"q": "x"}),),
261
+ ),
262
+ Message(role="tool", content="result", tool_call_id="call_1"),
263
+ ],
264
+ )
265
+ sent = fake_runner.chat_calls[0].messages[0]
266
+ assert sent["role"] == "assistant"
267
+ assert sent["tool_calls"][0]["id"] == "call_1"
268
+ assert sent["tool_calls"][0]["type"] == "function"
269
+ assert sent["tool_calls"][0]["function"]["name"] == "search"
270
+ assert json.loads(sent["tool_calls"][0]["function"]["arguments"]) == {"q": "x"}
271
+
272
+
230
273
  @pytest.mark.asyncio
231
274
  async def test_call_finish_reason_length_maps_to_max_tokens(
232
275
  client: OpenAIClient,
@@ -325,6 +368,21 @@ async def test_call_passes_tool_specs(
325
368
  assert tools_arg[0]["function"]["parameters"] == schema
326
369
 
327
370
 
371
+ @pytest.mark.asyncio
372
+ async def test_call_rejects_illegal_tool_name_locally(
373
+ client: OpenAIClient,
374
+ fake_runner: FakeOpenAIRunner,
375
+ ) -> None:
376
+ """bug-017: an illegal tool name fails locally before any request."""
377
+ with pytest.raises(ToolNameInvalidError, match="kb_search"):
378
+ await client.call(
379
+ system="",
380
+ messages=[_user("hi")],
381
+ tools=[ToolSpec(name="kb.search", description="d", schema={"type": "object"})],
382
+ )
383
+ assert fake_runner.chat_calls == []
384
+
385
+
328
386
  # ----------------------------------------------------------------------
329
387
  # Streaming
330
388
  # ----------------------------------------------------------------------