commit
8b22f2b142
8 changed files with 121 additions and 30 deletions
|
|
@ -4,4 +4,4 @@ OPENAI_URL = "https://api.openai.com/v1"
|
|||
DEFAULT_OPENAI_MODEL = "gpt-4.1"
|
||||
DEFAULT_GOOGLE_MODEL = "models/gemini-2.5-flash"
|
||||
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514"
|
||||
DEFAULT_CODEX_MODEL = "gpt-5.2-codex"
|
||||
DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini"
|
||||
|
|
|
|||
|
|
@ -100,6 +100,55 @@ class LLMClient:
|
|||
return False
|
||||
return parse_bool_or_none(get_web_grounding_env()) or False
|
||||
|
||||
def web_search_enabled_for_request(self, web_search: bool) -> bool:
|
||||
"""Attach SearchWebTool only when the user enabled web search for this request.
|
||||
|
||||
Controlled solely by the presentation ``web_search`` flag (Advanced settings).
|
||||
Legacy ``WEB_GROUNDING`` / settings toggles are not consulted here so a saved
|
||||
false there cannot disable per-deck web search.
|
||||
"""
|
||||
if not web_search:
|
||||
return False
|
||||
if self.llm_provider in (LLMProvider.OLLAMA, LLMProvider.CUSTOM):
|
||||
return False
|
||||
return True
|
||||
|
||||
def outline_uses_prefetched_web_facts(self, web_search: bool) -> bool:
|
||||
"""Chat Completions + json_schema rarely invoke custom function tools.
|
||||
|
||||
For OpenAI and Codex we prefetch via the Responses API (``web_search_preview``)
|
||||
and attach the result as context so Advanced settings **Web search** still
|
||||
grounds outlines without relying on ``SearchWebTool`` in the same call.
|
||||
"""
|
||||
if not self.web_search_enabled_for_request(web_search):
|
||||
return False
|
||||
return self.llm_provider in (LLMProvider.OPENAI, LLMProvider.CODEX)
|
||||
|
||||
async def prefetch_outline_web_facts(
|
||||
self,
|
||||
content: str,
|
||||
additional_context: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if self.llm_provider not in (LLMProvider.OPENAI, LLMProvider.CODEX):
|
||||
return None
|
||||
parts = [(content or "").strip(), (additional_context or "").strip()]
|
||||
topic = "\n\n".join(p for p in parts if p)
|
||||
if not topic:
|
||||
topic = "general presentation topic"
|
||||
topic = topic[:12000]
|
||||
query = (
|
||||
"Search the web and summarize the most relevant current facts, statistics, "
|
||||
"and notable recent developments for this presentation topic. Use concise "
|
||||
"bullet points; include approximate dates or time ranges when known.\n\n"
|
||||
f"Topic:\n{topic}"
|
||||
)
|
||||
try:
|
||||
text = await self._search_openai(query)
|
||||
out = (text or "").strip()
|
||||
return out or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ? Disable thinking
|
||||
def disable_thinking(self) -> bool:
|
||||
return parse_bool_or_none(get_disable_thinking_env()) or False
|
||||
|
|
@ -1753,7 +1802,7 @@ class LLMClient:
|
|||
current_arguments = None
|
||||
|
||||
has_response_schema_tool_call = False
|
||||
async for event in await client.chat.completions.create(
|
||||
completion_kwargs: Dict[str, Any] = dict(
|
||||
model=model,
|
||||
messages=[message.model_dump() for message in messages],
|
||||
max_completion_tokens=max_tokens,
|
||||
|
|
@ -1774,7 +1823,11 @@ class LLMClient:
|
|||
),
|
||||
extra_body=extra_body,
|
||||
stream=True,
|
||||
):
|
||||
)
|
||||
if all_tools:
|
||||
completion_kwargs["tool_choice"] = "auto"
|
||||
completion_kwargs["parallel_tool_calls"] = True
|
||||
async for event in await client.chat.completions.create(**completion_kwargs):
|
||||
event: OpenAIChatCompletionChunk = event
|
||||
if not event.choices:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class LLMToolCallsHandler:
|
|||
self.dynamic_tools.append(tool)
|
||||
|
||||
match self.client.llm_provider:
|
||||
case LLMProvider.OPENAI | LLMProvider.OLLAMA | LLMProvider.CUSTOM:
|
||||
case LLMProvider.OPENAI | LLMProvider.OLLAMA | LLMProvider.CUSTOM | LLMProvider.CODEX:
|
||||
return self.parse_tool_openai(tool, strict)
|
||||
case LLMProvider.ANTHROPIC:
|
||||
return self.parse_tool_anthropic(tool)
|
||||
|
|
@ -63,7 +63,7 @@ class LLMToolCallsHandler:
|
|||
return self.parse_tool_google(tool)
|
||||
case _:
|
||||
raise ValueError(
|
||||
f"LLM provider must be either openai, anthropic, or google"
|
||||
"LLM provider must be one of: openai, anthropic, google, codex, ollama, custom"
|
||||
)
|
||||
|
||||
def parse_tool_openai(
|
||||
|
|
@ -181,7 +181,7 @@ class LLMToolCallsHandler:
|
|||
# Search web tool call handler
|
||||
async def search_web_tool_call_handler(self, arguments: str) -> str:
|
||||
match self.client.llm_provider:
|
||||
case LLMProvider.OPENAI:
|
||||
case LLMProvider.OPENAI | LLMProvider.CODEX:
|
||||
return await self.search_web_tool_call_handler_openai(arguments)
|
||||
case LLMProvider.ANTHROPIC:
|
||||
return await self.search_web_tool_call_handler_anthropic(arguments)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ def get_system_prompt(
|
|||
instructions: Optional[str] = None,
|
||||
include_title_slide: bool = True,
|
||||
include_table_of_contents: bool = False,
|
||||
web_search: bool = False,
|
||||
):
|
||||
verbosity_instruction = (
|
||||
"Slide content should be abound 20 words but detailed enough to generate a good slide."
|
||||
|
|
@ -40,6 +41,27 @@ def get_system_prompt(
|
|||
)
|
||||
toc_block = f"{toc_instruction}\n" if toc_instruction else ""
|
||||
|
||||
if web_search:
|
||||
tools_hint = "Try to use available tools when they improve accuracy.\n"
|
||||
web_block = (
|
||||
"Web search is enabled: use any \"## Web research (current sources)\" section in Context when present, "
|
||||
"and call SearchWebTool when it is available for fresh facts.\n"
|
||||
)
|
||||
else:
|
||||
tools_hint = ""
|
||||
web_block = "Do not use web search for this outline; rely on Content and Context only.\n"
|
||||
|
||||
url_line = (
|
||||
"Only include URLs if they appear in Content, Context, or a \"## Web research (current sources)\" block.\n"
|
||||
if web_search
|
||||
else "Only include URLs if they appear in the provided content/context.\n"
|
||||
)
|
||||
data_line = (
|
||||
"Ground slide data in Content and Context, and in \"## Web research (current sources)\" when that block is present.\n"
|
||||
if web_search
|
||||
else "Make sure data used is strictly from the provided content/context.\n"
|
||||
)
|
||||
|
||||
slide_outline_structure = (
|
||||
"Each slide content:\n"
|
||||
" - Must have a ## title.\n"
|
||||
|
|
@ -60,11 +82,13 @@ def get_system_prompt(
|
|||
"If 'auto-detect' is used, figure it out from the content/context.\n"
|
||||
f"{title_slide_instruction}\n"
|
||||
f"{toc_block}"
|
||||
f"{tools_hint}"
|
||||
f"{web_block}"
|
||||
f"{slide_outline_structure}\n"
|
||||
"Slide content must not contain any presentation branding/styling information.\n"
|
||||
"Title slide must only contain title, presenter name, date and overview.\n"
|
||||
"Only include URLs if they appear in the provided content/context.\n"
|
||||
"Make sure data used is strictly from the provided content/context.\n"
|
||||
f"{url_line}"
|
||||
f"{data_line}"
|
||||
"Make sure data is consistent across all slides."
|
||||
)
|
||||
|
||||
|
|
@ -124,6 +148,7 @@ def get_messages(
|
|||
instructions: Optional[str] = None,
|
||||
include_title_slide: bool = True,
|
||||
include_table_of_contents: bool = False,
|
||||
web_search: bool = False,
|
||||
):
|
||||
return [
|
||||
LLMSystemMessage(
|
||||
|
|
@ -133,6 +158,7 @@ def get_messages(
|
|||
instructions,
|
||||
include_title_slide,
|
||||
include_table_of_contents,
|
||||
web_search=web_search,
|
||||
),
|
||||
),
|
||||
LLMUserMessage(
|
||||
|
|
@ -171,6 +197,21 @@ async def generate_ppt_outline(
|
|||
|
||||
client = LLMClient()
|
||||
|
||||
merged_context = additional_context
|
||||
if client.outline_uses_prefetched_web_facts(web_search):
|
||||
facts = await client.prefetch_outline_web_facts(content, additional_context)
|
||||
if facts:
|
||||
merged_context = (
|
||||
f"{(additional_context or '').strip()}\n\n## Web research (current sources)\n{facts}"
|
||||
if (additional_context or "").strip()
|
||||
else f"## Web research (current sources)\n{facts}"
|
||||
)
|
||||
|
||||
use_search_tool = (
|
||||
client.web_search_enabled_for_request(web_search)
|
||||
and not client.outline_uses_prefetched_web_facts(web_search)
|
||||
)
|
||||
|
||||
try:
|
||||
async for chunk in client.stream_structured(
|
||||
model,
|
||||
|
|
@ -178,20 +219,17 @@ async def generate_ppt_outline(
|
|||
content,
|
||||
n_slides,
|
||||
language,
|
||||
additional_context,
|
||||
merged_context,
|
||||
tone,
|
||||
verbosity,
|
||||
instructions,
|
||||
include_title_slide,
|
||||
include_table_of_contents,
|
||||
web_search=bool(web_search),
|
||||
),
|
||||
response_model.model_json_schema(),
|
||||
strict=True,
|
||||
tools=(
|
||||
[SearchWebTool]
|
||||
if (client.enable_web_grounding() and web_search)
|
||||
else None
|
||||
),
|
||||
tools=([SearchWebTool] if use_search_tool else None),
|
||||
):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@ interface CodexModel {
|
|||
}
|
||||
|
||||
const CHATGPT_MODELS: CodexModel[] = [
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1-Codex-Max" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4-Mini" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1-Codex-Mini" },
|
||||
];
|
||||
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.4-mini";
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini";
|
||||
|
||||
export default function CodexConfig({
|
||||
codexModel,
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ const AdvanceSettings = ({ config, onConfigChange }: ConfigurationSelectsProps)
|
|||
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts.</p>
|
||||
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts. Turn on and click Save—this toggle alone controls web search for this deck.</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ export function ConfigurationSelects({
|
|||
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts.</p>
|
||||
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts. Turn on and click Save—this toggle alone controls web search for this deck.</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
|
|
|
|||
|
|
@ -33,16 +33,16 @@ interface CodexModel {
|
|||
}
|
||||
|
||||
export const CHATGPT_MODELS: CodexModel[] = [
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4 mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1-Codex-Max" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4-Mini" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1-Codex-Mini" },
|
||||
];
|
||||
|
||||
export const DEFAULT_CODEX_MODEL = "gpt-5.4-mini";
|
||||
export const DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini";
|
||||
|
||||
export default function CodexConfig({
|
||||
codexModel,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue