Merge pull request #427 from presenton/feat/some-fixes

Feat/some fixes
This commit is contained in:
Sudip Parajuli 2026-03-02 20:14:41 +05:45 committed by GitHub
commit 05ea7dea5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 109 additions and 43 deletions

View file

@ -1,12 +1,12 @@
{
"name": "presenton",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "presenton",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"hasInstallScript": true,
"dependencies": {
"@tailwindcss/cli": "^4.1.5",

View file

@ -1,7 +1,7 @@
{
"name": "presenton",
"productName": "Presenton Open Source",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"main": "app_dist/main.js",
"description": "Open-Source AI Presentation Generator",
"homepage": "https://presenton.ai",

View file

@ -317,9 +317,9 @@ async def stream_presentation(
# This will mutate slide and add placeholder assets
process_slide_add_placeholder_assets(slide)
# This will mutate slide
# This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation
async_assets_generation_tasks.append(
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
)
yield SSEResponse(
@ -721,9 +721,9 @@ async def generate_presentation_handler(
slides.append(slide)
batch_slides.append(slide)
# Start asset fetch tasks for just-generated slides so they run while next batch is processed
# Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls
asset_tasks = [
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
for slide in batch_slides
]
async_assets_generation_tasks.extend(asset_tasks)

View file

@ -66,6 +66,7 @@ from utils.set_env import (
from utils.llm_provider import get_llm_provider, get_model
from utils.parsers import parse_bool_or_none
from utils.schema_utils import (
ensure_array_schemas_have_items,
ensure_strict_json_schema,
flatten_json_schema,
remove_titles_from_schema,
@ -702,6 +703,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
all_tools = []
@ -1599,6 +1601,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
@ -1793,28 +1796,16 @@ class LLMClient:
"""
client: AsyncOpenAI = self._client
response_schema = response_format
# Apply strict schema once at root
# Apply strict schema once at root (includes array "items" fix at lines 135155).
if strict and depth == 0:
response_schema = ensure_strict_json_schema(
response_schema,
path=(),
root=response_schema,
)
# Codex Responses API requires all array schemas to specify `items`.
def _fix_arrays(node: Any) -> Any:
if isinstance(node, dict):
# Add default items for arrays missing them
if node.get("type") == "array" and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _fix_arrays(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _fix_arrays(value)
return node
response_schema = _fix_arrays(response_schema)
# When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0).
else:
response_schema = ensure_array_schemas_have_items(response_schema)
# Responses API tool format: flat {type, name, description, parameters}
response_schema_tool = {

View file

@ -134,11 +134,25 @@ def ensure_strict_json_schema(
# arrays
# { 'type': 'array', 'items': {...} }
# OpenAI requires array schemas to have "items". Zod tuples may emit prefixItems only.
items = json_schema.get("items")
if isinstance(items, dict):
json_schema["items"] = ensure_strict_json_schema(
items, path=(*path, "items"), root=root
)
elif typ == "array":
prefix_items = json_schema.get("prefixItems")
if (
isinstance(prefix_items, list)
and len(prefix_items) > 0
and isinstance(prefix_items[0], dict)
):
json_schema["items"] = ensure_strict_json_schema(
prefix_items[0], path=(*path, "items"), root=root
)
json_schema.pop("prefixItems", None)
else:
json_schema["items"] = {"type": "string"}
# unions
any_of = json_schema.get("anyOf")
@ -281,6 +295,34 @@ def flatten_json_schema(schema: dict) -> dict:
return result
def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]:
"""
Recursively ensure every JSON schema node with type="array" has an "items" key.
Codex Responses API requires array schemas to specify items. Mutates a deep copy.
"""
result = deepcopy(schema)
def _is_array_schema_type(type_value: Any) -> bool:
if type_value == "array":
return True
if isinstance(type_value, list):
return "array" in type_value
return False
def _ensure(node: Any) -> Any:
if isinstance(node, dict):
if _is_array_schema_type(node.get("type")) and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _ensure(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _ensure(value)
return node
return _ensure(result)
def remove_titles_from_schema(schema: dict) -> dict[str, Any]:
def _strip_titles(node: Any) -> Any:

View file

@ -317,9 +317,9 @@ async def stream_presentation(
# This will mutate slide and add placeholder assets
process_slide_add_placeholder_assets(slide)
# This will mutate slide
# This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation
async_assets_generation_tasks.append(
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
)
yield SSEResponse(
@ -716,9 +716,9 @@ async def generate_presentation_handler(
slides.append(slide)
batch_slides.append(slide)
# Start asset fetch tasks for just-generated slides so they run while next batch is processed
# Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls
asset_tasks = [
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
for slide in batch_slides
]
async_assets_generation_tasks.extend(asset_tasks)

View file

@ -66,6 +66,7 @@ from utils.set_env import (
from utils.llm_provider import get_llm_provider, get_model
from utils.parsers import parse_bool_or_none
from utils.schema_utils import (
ensure_array_schemas_have_items,
ensure_strict_json_schema,
flatten_json_schema,
remove_titles_from_schema,
@ -702,6 +703,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
all_tools = []
@ -1599,6 +1601,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
@ -1793,28 +1796,16 @@ class LLMClient:
"""
client: AsyncOpenAI = self._client
response_schema = response_format
# Apply strict schema once at root
# Apply strict schema once at root (includes array "items" fix in ensure_strict_json_schema).
if strict and depth == 0:
response_schema = ensure_strict_json_schema(
response_schema,
path=(),
root=response_schema,
)
# Codex Responses API requires all array schemas to specify `items`.
def _fix_arrays(node: Any) -> Any:
if isinstance(node, dict):
# Add default items for arrays missing them
if node.get("type") == "array" and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _fix_arrays(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _fix_arrays(value)
return node
response_schema = _fix_arrays(response_schema)
# When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0).
else:
response_schema = ensure_array_schemas_have_items(response_schema)
# Responses API tool format: flat {type, name, description, parameters}
response_schema_tool = {

View file

@ -185,6 +185,20 @@ def ensure_strict_json_schema(
items, path=(*path, "items"), root=root
)
elif typ == "array":
prefix_items = json_schema.get("prefixItems")
if (
isinstance(prefix_items, list)
and len(prefix_items) > 0
and isinstance(prefix_items[0], dict)
):
json_schema["items"] = ensure_strict_json_schema(
prefix_items[0], path=(*path, "items"), root=root
)
json_schema.pop("prefixItems", None)
else:
json_schema["items"] = {"type": "string"}
# unions
any_of = json_schema.get("anyOf")
if isinstance(any_of, list):
@ -326,6 +340,34 @@ def flatten_json_schema(schema: dict) -> dict:
return result
def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]:
"""
Recursively ensure every JSON schema node with type="array" has an "items" key.
Codex Responses API requires array schemas to specify items. Mutates a deep copy.
"""
result = deepcopy(schema)
def _is_array_schema_type(type_value: Any) -> bool:
if type_value == "array":
return True
if isinstance(type_value, list):
return "array" in type_value
return False
def _ensure(node: Any) -> Any:
if isinstance(node, dict):
if _is_array_schema_type(node.get("type")) and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _ensure(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _ensure(value)
return node
return _ensure(result)
def remove_titles_from_schema(schema: dict) -> dict[str, Any]:
def _strip_titles(node: Any) -> Any: