Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/utils/conversation_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
)
from utils.conversations import (
append_turn_items_to_conversation,
build_add_items_request,
get_all_conversation_items,
)
from utils.token_estimator import (
Expand Down Expand Up @@ -275,16 +276,17 @@ async def _write_summary_marker(
summary_text: str,
) -> None:
"""Write the summary into the conversation as a recognizable marker message."""
marker_item: dict[str, Any] = {
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": f"{MARKER_SENTINEL} {summary_text}"}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to plaintext message

],
}
await client.conversations.items.create(
conversation_id,
items=[marker_item],
add_items_request=build_add_items_request(
[
{
"type": "message",
"role": "user",
"content": f"{MARKER_SENTINEL} {summary_text}",
}
]
),
)


Expand Down
68 changes: 53 additions & 15 deletions src/utils/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from typing import Any, Literal, Optional, cast

from fastapi import HTTPException
from ogx_api import OpenAIResponseMessage, OpenAIResponseOutput
from ogx_api import OpenAIResponseOutput
from ogx_client import APIConnectionError, APIStatusError, AsyncOgxClient
from ogx_client.models.add_items_request import AddItemsRequest
from ogx_client.models.open_ai_response_input_function_tool_call_output import (
OpenAIResponseInputFunctionToolCallOutput as FunctionCallOutput,
)
Expand Down Expand Up @@ -47,6 +48,7 @@
from ogx_client.models.open_ai_response_output_message_web_search_tool_call import (
OpenAIResponseOutputMessageWebSearchToolCall as WebSearchCall,
)
from pydantic import ValidationError

from constants import DEFAULT_RAG_TOOL
from models.api.responses.error import (
Expand All @@ -66,6 +68,40 @@
type FunctionCallOutputContent = str | list[FunctionCallOutputPart]


def to_conversation_item(data: dict[str, Any]) -> Optional[ConversationItem]:
"""Attempt to parse a raw dict into a oneOf wrapper.

Parameters:
data: Raw dict to parse.

Returns:
ConversationItem if parsing succeeds,
None otherwise.
"""
try:
return ConversationItem.from_dict(data)

@asimurka asimurka Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New client types use automatic anyOf validators that create the actual model instances from plain dicts. This is preferable to importing internal client models with obscurely long names.

except (ValidationError, ValueError):
return None


def build_add_items_request(items: Sequence[dict[str, Any]]) -> AddItemsRequest:
"""Build an ``AddItemsRequest`` from conversation items.

Parameters:
items: Conversation items to append.

Returns:
Request body for items.create.
"""
return AddItemsRequest(
items=[
conversation_item
for item in items
if (conversation_item := to_conversation_item(item)) is not None
]
)


def _extract_text_from_content(content: Any) -> str:
"""Extract text content from message content.

Expand Down Expand Up @@ -503,21 +539,17 @@ async def append_turn_items_to_conversation(
llm_output: Output from the LLM: a list of OpenAIResponseOutput.
"""
if isinstance(user_input, str):
user_message = OpenAIResponseMessage(
role="user",
content=user_input,
)
user_items = [user_message.model_dump()]
items: list[dict[str, Any]] = [
{"type": "message", "role": "user", "content": user_input}
]
else:
user_items = [item.model_dump() for item in user_input]
items = [item.model_dump(exclude_none=True) for item in user_input]

output_items = [item.model_dump() for item in llm_output]

items = user_items + output_items
items.extend(item.model_dump(exclude_none=True) for item in llm_output)
try:
await client.conversations.items.create(
conversation_id,
items=items,
add_items_request=build_add_items_request(items),
)
except APIConnectionError as e:
error_response = ServiceUnavailableResponse(
Expand Down Expand Up @@ -590,10 +622,16 @@ async def append_turn_to_conversation(
try:
await client.conversations.items.create(
conversation_id,
items=[
{"type": "message", "role": "user", "content": user_message},
{"type": "message", "role": "assistant", "content": assistant_message},
],
add_items_request=build_add_items_request(
[
{"type": "message", "role": "user", "content": user_message},
{
"type": "message",
"role": "assistant",
"content": assistant_message,
},
]
),
)
except APIConnectionError as e:
error_response = ServiceUnavailableResponse(
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/endpoints/test_model_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from authentication.interface import AuthTuple
from configuration import AppConfig
from models.api.requests import ModelFilter
from tests.integration.conftest import make_openai_model, make_openai_models_list_response
from tests.integration.conftest import (
make_openai_model,
make_openai_models_list_response,
)


@pytest.fixture(name="mock_ogx_client")
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/endpoints/test_responses_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@
from models.api.responses.successful import ResponsesResponse
from models.common.moderation import ShieldModerationBlocked
from models.common.responses.contexts import ResponsesContext
from tests.integration.conftest import make_openai_model, make_openai_models_list_response
from models.database.conversations import UserConversation, UserTurn
from tests.integration.conftest import (
make_openai_model,
make_openai_models_list_response,
)

MOCK_AUTH: AuthTuple = (
"00000000-0000-0000-0000-000",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
from configuration import AppConfig
from models.api.requests import QueryRequest
from models.common.query import Attachment
from tests.integration.conftest import make_openai_model, make_openai_models_list_response
from tests.integration.conftest import (
make_openai_model,
make_openai_models_list_response,
)


@pytest.fixture(name="mock_streaming_ogx_client")
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/app/endpoints/test_rags.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Unit tests for the /rags REST API endpoints."""

from collections.abc import Iterator
from pathlib import Path
from typing import Any, Iterator
from typing import Any

import pytest
from fastapi import HTTPException, Request, status
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/app/endpoints/test_vector_stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

# pylint: disable=too-many-lines

from typing import Any, Iterator
from collections.abc import Iterator
from typing import Any

import pytest
from fastapi import HTTPException, Request, status
Expand Down
103 changes: 87 additions & 16 deletions tests/unit/utils/test_conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from fastapi import HTTPException
from ogx_api import OpenAIResponseMessage
from ogx_client import APIConnectionError, APIStatusError
from ogx_client.models.add_items_request import AddItemsRequest
from ogx_client.models.open_ai_response_input_function_tool_call_output import (
OpenAIResponseInputFunctionToolCallOutput as FunctionCallOutput,
)
Expand All @@ -30,8 +31,10 @@
_function_call_output_to_str,
append_turn_items_to_conversation,
append_turn_to_conversation,
build_add_items_request,
build_conversation_turns_from_items,
get_all_conversation_items,
to_conversation_item,
)

# Default conversation start time for tests
Expand Down Expand Up @@ -809,6 +812,72 @@ def test_legacy_conversation_without_metadata(self, mocker: MockerFixture) -> No
assert turn.completed_at == "2024-01-01T10:00:00Z"


class TestToConversationItem:
"""Tests for to_conversation_item."""

def test_parses_message_dict(self) -> None:
"""Valid message dict becomes a conversation item."""
item = to_conversation_item(
{"type": "message", "role": "user", "content": "Hello"}
)
assert item is not None
assert item.type == "message"
assert item.role == "user"
assert item.content == "Hello"

def test_parses_model_dump_payload(self) -> None:
"""model_dump() of an ogx_api message is accepted."""
payload = OpenAIResponseMessage(
type="message",
role="assistant",
content="Hi",
).model_dump(exclude_none=True)
item = to_conversation_item(payload)
assert item is not None
assert item.role == "assistant"
assert item.content == "Hi"

def test_returns_none_for_invalid_payload(self) -> None:
"""Unrecognized oneOf payload returns None instead of raising."""
assert to_conversation_item({"type": "not_a_real_variant"}) is None


class TestBuildAddItemsRequest:
"""Tests for build_add_items_request."""

def test_builds_request_from_message_dicts(self) -> None:
"""Dicts are validated into AddItemsRequest items."""
request = build_add_items_request(
[
{"type": "message", "role": "user", "content": "Hello"},
{
"type": "message",
"role": "assistant",
"content": "I cannot help with that",
},
]
)
assert isinstance(request, AddItemsRequest)
items = list(request)
assert len(items) == 2
assert items[0].type == "message" and items[0].role == "user"
assert items[0].content == "Hello"
assert items[1].type == "message" and items[1].role == "assistant"
assert items[1].content == "I cannot help with that"

def test_skips_invalid_dicts(self) -> None:
"""Invalid payloads are filtered out of the request."""
request = build_add_items_request(
[
{"type": "message", "role": "user", "content": "ok"},
{"type": "not_a_real_variant"},
]
)
items = list(request)
assert len(items) == 1
assert items[0].content == "ok"


class TestAppendTurnItemsToConversation: # pylint: disable=too-few-public-methods
"""Tests for append_turn_items_to_conversation function."""

Expand All @@ -835,12 +904,14 @@ async def test_appends_user_input_and_llm_output(
mock_client.conversations.items.create.assert_called_once()
call_args = mock_client.conversations.items.create.call_args
assert call_args[0][0] == "conv-123"
items = call_args[1]["items"]
request = call_args[1]["add_items_request"]
assert isinstance(request, AddItemsRequest)
items = list(request)
assert len(items) == 2
assert items[0]["type"] == "message" and items[0]["role"] == "user"
assert items[0]["content"] == "Hello"
assert items[1]["type"] == "message" and items[1]["role"] == "assistant"
assert items[1]["content"] == "I cannot help with that"
assert items[0].type == "message" and items[0].role == "user"
assert items[0].content == "Hello"
assert items[1].type == "message" and items[1].role == "assistant"
assert items[1].content == "I cannot help with that"


class TestAppendTurnToConversation: # pylint: disable=too-few-public-methods
Expand All @@ -861,17 +932,17 @@ async def test_appends_user_and_assistant_messages(
assistant_message="I cannot help with that",
)

mock_client.conversations.items.create.assert_called_once_with(
"conv-123",
items=[
{"type": "message", "role": "user", "content": "Hello"},
{
"type": "message",
"role": "assistant",
"content": "I cannot help with that",
},
],
)
mock_client.conversations.items.create.assert_called_once()
call_args = mock_client.conversations.items.create.call_args
assert call_args[0][0] == "conv-123"
request = call_args[1]["add_items_request"]
assert isinstance(request, AddItemsRequest)
items = list(request)
assert len(items) == 2
assert items[0].type == "message" and items[0].role == "user"
assert items[0].content == "Hello"
assert items[1].type == "message" and items[1].role == "assistant"
assert items[1].content == "I cannot help with that"


class TestGetAllConversationItems:
Expand Down
Loading