Merge pull request #519 from presenton/fix/web-search

Fix/web search
This commit is contained in:
Sudip Parajuli 2026-04-16 19:20:37 +05:45 committed by GitHub
commit 8b22f2b142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 121 additions and 30 deletions

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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,

View file

@ -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 Savethis toggle alone controls web search for this deck.</p>
</div>
{/* Instructions */}

View file

@ -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 Savethis toggle alone controls web search for this deck.</p>
</div>
{/* Instructions */}

View file

@ -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,