commit
05ea7dea5b
8 changed files with 109 additions and 43 deletions
4
electron/package-lock.json
generated
4
electron/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 135–155).
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue