Skip to content

Commit f8967ca

Browse files
authored
[Feat] adds support for openai responses api logging and adds another method to querybuilder for fetching specific prompt version (#91)
1 parent 3e10862 commit f8967ca

File tree

9 files changed

+417
-8
lines changed

9 files changed

+417
-8
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,33 +60,49 @@ See [cookbook/agno_agent.py](cookbook/agno_agent.py) for an example of tracing a
6060

6161
## Version changelog
6262

63+
### 3.12.0
64+
65+
- feat: Added support for OpenAI Responses API format in addition to Chat Completion API
66+
- feat: Added TTL-based caching (60s) for prompt version number single-condition fetches
67+
- feat: Added new `prompt_version_number()` method in QueryBuilder for convenient version-specific queries
68+
- improvement: Enhanced `generation_parser` to detect and handle OpenAI Responses API structure
69+
6370
### 3.11.4
71+
6472
- fix: Fixes race condition in LiveKit realtime tracing
6573
- fix: Fixes import errors for Gemini and Google realtime session imports
6674

6775
### 3.11.3
76+
6877
- fix: Fixed Nested Spans issue for Google ADK
6978

7079
### 3.11.2
80+
7181
- fix: Fixed Google ADK integration to support spans for agent hand offs and tool calls.
7282

7383
### 3.11.1
84+
7485
- feat: Added `ContainerManager` to Langchain to manage containers
7586
- fix: Fixes `trace.end` in Langchain integration `MaximLangchainTracer`
7687

7788
### 3.11.0
89+
7890
- feat: Added observability for google adk
7991

8092
### 3.10.10
93+
8194
- feat: Added case for `commit_user_turn` for LiveKit logs
8295

8396
### 3.10.9
97+
8498
- fix: Fixed session audio silences for LiveKit Realtime session implementation
8599

86100
### 3.10.8
101+
87102
- feat: Added Pydantic AI Single Line Integration
88103

89104
### 3.10.7
105+
90106
- fix: Fixes local data test runs
91107

92108
### 3.10.6

maxim/apis/maxim_apis.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def __make_network_call(
309309
method, endpoint, body, headers, self.max_retries
310310
)
311311

312-
def get_prompt(self, id: str) -> VersionAndRulesWithPromptId:
312+
def get_prompt(self, id: str, prompt_version_number: Optional[int] = None) -> VersionAndRulesWithPromptId:
313313
"""
314314
Get a prompt by ID.
315315
@@ -323,9 +323,10 @@ def get_prompt(self, id: str) -> VersionAndRulesWithPromptId:
323323
Exception: If the request fails
324324
"""
325325
try:
326-
res = self.__make_network_call(
327-
method="GET", endpoint=f"/api/sdk/v4/prompts?promptId={id}"
328-
)
326+
endpoint = f"/api/sdk/v4/prompts?promptId={id}"
327+
if prompt_version_number is not None:
328+
endpoint += f"&promptVersionNumber={prompt_version_number}"
329+
res = self.__make_network_call(method="GET", endpoint=endpoint)
329330
data = json.loads(res.decode())["data"]
330331
return VersionAndRulesWithPromptId.from_dict(data)
331332
except httpx.HTTPStatusError as e:

maxim/logger/components/generation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,9 @@ def convert_result(
699699
and result_dict["object"] == "text.completion"
700700
):
701701
raise ValueError("Text completion is not yet supported.")
702+
elif "object" in result_dict and result_dict["object"] == "response":
703+
# OpenAI Responses API format - return as-is for logging
704+
return result_dict
702705
return result
703706

704707
def result(self, result: Any):

maxim/logger/parsers/generation_parser.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def parse_tool_calls(tool_calls_data):
5656
Returns:
5757
The parsed tool calls.
5858
"""
59-
if ChatCompletionMessageToolCall is not None and isinstance(tool_calls_data, ChatCompletionMessageToolCall):
59+
if ChatCompletionMessageToolCall is not None and isinstance(
60+
tool_calls_data, ChatCompletionMessageToolCall
61+
):
6062
validate_type(tool_calls_data.id, str, "id")
6163
validate_type(tool_calls_data.type, str, "type")
6264
parse_function_call(tool_calls_data.function)
@@ -217,10 +219,58 @@ def default_json_serializer(o: Any) -> Any:
217219
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
218220

219221

222+
def is_openai_response_structure(data: Any) -> bool:
223+
"""
224+
Check if data matches the general top-level shape of an OpenAI Responses API result.
225+
226+
The OpenAI Responses API structure includes:
227+
- id: string identifier
228+
- object: string (must be "response")
229+
- created_at: integer timestamp
230+
- status: string (e.g., "completed", "in_progress")
231+
- output: list of output items
232+
- usage: dict with token usage
233+
234+
Args:
235+
data: The dictionary to check.
236+
237+
Returns:
238+
True if the data matches the OpenAI Responses API structure, False otherwise.
239+
"""
240+
if not isinstance(data, dict):
241+
return False
242+
243+
# Check for required OpenAI Responses API fields
244+
required_fields = {
245+
"id": str,
246+
"object": str,
247+
"created_at": (int, float),
248+
"status": str,
249+
"output": list,
250+
"usage": dict,
251+
}
252+
253+
for field, expected_type in required_fields.items():
254+
if field not in data:
255+
return False
256+
257+
value = data[field]
258+
if not isinstance(value, expected_type):
259+
return False
260+
261+
# Verify that object is specifically "response"
262+
if data.get("object") != "response":
263+
return False
264+
265+
return True
266+
267+
220268
def parse_result(data: Any) -> Dict[str, Any]:
221269
"""
222270
Parse result from a dictionary.
223271
272+
Supports both OpenAI Chat Completion API and OpenAI Responses API result structures.
273+
224274
Args:
225275
data: The dictionary to parse.
226276
@@ -229,6 +279,14 @@ def parse_result(data: Any) -> Dict[str, Any]:
229279
"""
230280
if not isinstance(data, dict):
231281
raise ValueError("Text completion is not supported.")
282+
283+
# Check if this is an OpenAI Responses API result structure
284+
if is_openai_response_structure(data):
285+
# For Responses API results, return as-is without deep validation
286+
# Only the general top-level shape is validated by is_openai_response_structure
287+
return data
288+
289+
# Otherwise, process as Chat Completion API result (existing behavior)
232290
validate_type(data.get("id"), str, "id")
233291
validate_optional_type(data.get("object"), str, "object")
234292
validate_type(data.get("created"), int, "created")

maxim/maxim.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from .scribe import scribe
5050
from .test_runs import TestRunBuilder
5151
from .version import current_version
52+
from .expiring_key_value_store import ExpiringKeyValueStore
5253

5354

5455
class ConfigDict(TypedDict, total=False):
@@ -174,6 +175,8 @@ def __init__(self, config: Union[Config, ConfigDict, None] = None):
174175
self.__loggers: Dict[str, Logger] = {}
175176
self.prompt_management = final_config.get("prompt_management", False)
176177
self.__cache = final_config.get("cache", MaximInMemoryCache())
178+
# Local TTL cache for promptVersionNumber single-condition fetches
179+
self.__prompt_version_by_number_cache: ExpiringKeyValueStore[Prompt] = ExpiringKeyValueStore()
177180
if self.prompt_management:
178181
self.__sync_thread = threading.Thread(target=self.__sync_timer)
179182
self.__sync_thread.daemon = True
@@ -748,6 +751,34 @@ def get_prompt(self, id: str, rule: QueryRule) -> Optional[RunnablePrompt]:
748751
raise Exception(
749752
"prompt_management is disabled. You can enable it by initializing Maxim with Config(...prompt_management=True)."
750753
)
754+
# First, check if this is a single-condition promptVersionNumber query
755+
try:
756+
parsed_rules = parse_incoming_query(rule.query)
757+
except Exception:
758+
parsed_rules = []
759+
if len(parsed_rules) == 1 and parsed_rules[0].field == "promptVersionNumber" and parsed_rules[0].operator == "=":
760+
version_number = None
761+
try:
762+
version_number = int(parsed_rules[0].value)
763+
except Exception:
764+
version_number = None
765+
if version_number is not None:
766+
cache_key = f"pvnum:{id}:{version_number}"
767+
cached_prompt = self.__prompt_version_by_number_cache.get(cache_key)
768+
if cached_prompt is not None:
769+
return RunnablePrompt(cached_prompt, self.maxim_api)
770+
# Fetch prompt only for the specific version
771+
version_and_rules_with_prompt_id = self.maxim_api.get_prompt(id, version_number)
772+
if len(version_and_rules_with_prompt_id.versions) == 0:
773+
return None
774+
specific = next((v for v in version_and_rules_with_prompt_id.versions if v.version == version_number), None)
775+
if specific is None:
776+
return None
777+
formatted = self.__format_prompt(specific)
778+
# Cache for 60 seconds
779+
self.__prompt_version_by_number_cache.set(cache_key, formatted, 60)
780+
return RunnablePrompt(formatted, self.maxim_api)
781+
751782
key = self.__get_cache_key("PROMPT", id)
752783
version_and_rules_with_prompt_id = self.__get_prompt_from_cache(key)
753784
if version_and_rules_with_prompt_id is None:

maxim/models/query_builder.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ def tag(self, key: str, value: Union[str, int, bool], enforce: bool = False) ->
104104
self.query += f"{'!!' if enforce else ''}{key}={value}"
105105
return self
106106

107+
def prompt_version_number(self, number: int) -> 'QueryBuilder':
108+
"""
109+
Adds a rule to fetch a specific prompt version by its numeric version.
110+
111+
Args:
112+
number (int): The version number of the prompt to fetch.
113+
114+
Returns:
115+
QueryBuilder: The current QueryBuilder instance for method chaining.
116+
"""
117+
if len(self.query) > 0:
118+
self.query += ","
119+
self.query += f"promptVersionNumber={number}"
120+
return self
121+
107122
def build(self) -> QueryRule:
108123
"""
109124
Builds the final query rule.

0 commit comments

Comments
 (0)