Private
Public Access
feat(openai_schemas): add from_dict to ChatMessage, ToolCall, UsageStats
Infrastructure change required by Phase 5/6/7 of the type_alias_unfuck_20260626 track. The plan's migration pattern (var = Aggregate.from_dict(var)) requires from_dict on the target dataclasses. None existed for the openai_schemas classes, so this commit adds them. from_dict semantics: - Filter dict keys to only the dataclass fields (ignore extra keys like _est_tokens) - For ChatMessage: convert nested tool_calls list to tuple of ToolCall - For ToolCall: convert nested function dict to ToolCallFunction - For UsageStats: direct field mapping Field definitions unchanged. Behavior: zero impact on existing tests (no callers exist yet for from_dict on these classes). Tests: syntax check OK; manual instantiation confirms from_dict works.
This commit is contained in:
+24
-3
@@ -16,10 +16,14 @@ CONVENTION: 1-space indentation. NO COMMENTS.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, fields as dc_fields
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from src.type_aliases import JsonValue
|
from src.type_aliases import JsonValue, Metadata
|
||||||
|
|
||||||
|
|
||||||
|
def _from_dict_filter(cls: type, data: Metadata) -> Metadata:
|
||||||
|
return {k: v for k, v in data.items() if k in {f.name for f in dc_fields(cls)}}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -44,11 +48,16 @@ class ToolCall:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Metadata) -> "ToolCall":
|
||||||
|
fn = ToolCallFunction(**_from_dict_filter(ToolCallFunction, data.get("function", {})))
|
||||||
|
return cls(**{**_from_dict_filter(cls, data), "function": fn})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ChatMessage:
|
class ChatMessage:
|
||||||
role: str
|
role: str
|
||||||
content: str | list # str for text; list of content parts for multimodal (text + image_url, etc.)
|
content: str | list
|
||||||
tool_calls: Optional[tuple[ToolCall, ...]] = None
|
tool_calls: Optional[tuple[ToolCall, ...]] = None
|
||||||
tool_call_id: Optional[str] = None
|
tool_call_id: Optional[str] = None
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
@@ -63,6 +72,14 @@ class ChatMessage:
|
|||||||
d["name"] = self.name
|
d["name"] = self.name
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Metadata) -> "ChatMessage":
|
||||||
|
raw_tool_calls = data.get("tool_calls")
|
||||||
|
tool_calls = None
|
||||||
|
if raw_tool_calls is not None:
|
||||||
|
tool_calls = tuple(ToolCall.from_dict(tc) for tc in raw_tool_calls)
|
||||||
|
return cls(**{**_from_dict_filter(cls, data), "tool_calls": tool_calls})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UsageStats:
|
class UsageStats:
|
||||||
@@ -71,6 +88,10 @@ class UsageStats:
|
|||||||
cache_read_tokens: int = 0
|
cache_read_tokens: int = 0
|
||||||
cache_creation_tokens: int = 0
|
cache_creation_tokens: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Metadata) -> "UsageStats":
|
||||||
|
return cls(**_from_dict_filter(cls, data))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class NormalizedResponse:
|
class NormalizedResponse:
|
||||||
|
|||||||
Reference in New Issue
Block a user