diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index f5f4a8d3..80eea709 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -3,7 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware from api.lifespan import app_lifespan from api.middlewares import UserConfigEnvUpdateMiddleware from api.v1.ppt.router import API_V1_PPT_ROUTER -from api.v1.test.router import API_V1_TEST_ROUTER app = FastAPI(lifespan=app_lifespan) @@ -11,7 +10,6 @@ app = FastAPI(lifespan=app_lifespan) # Routers app.include_router(API_V1_PPT_ROUTER) -app.include_router(API_V1_TEST_ROUTER) # Middlewares origins = ["*"] diff --git a/servers/fastapi/api/v1/ppt/endpoints/ollama.py b/servers/fastapi/api/v1/ppt/endpoints/ollama.py index adde8669..0dafa3e1 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/ollama.py +++ b/servers/fastapi/api/v1/ppt/endpoints/ollama.py @@ -64,7 +64,7 @@ async def pull_model( # If the model is being pulled, return the model if saved_model_status: # If the model is being pulled, return the model - # ? If the model status is pulled in redis but was not found while listing pulled models, + # ? If the model status is pulled in database but was not found while listing pulled models, # ? it means the model was deleted and we need to pull it again if ( saved_model_status["status"] == "error" diff --git a/servers/fastapi/api/v1/test/router.py b/servers/fastapi/api/v1/test/router.py deleted file mode 100644 index 71b77d2b..00000000 --- a/servers/fastapi/api/v1/test/router.py +++ /dev/null @@ -1,29 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel, Field - -from models.llm_message import LLMUserMessage -from models.llm_tools import GetCurrentDatetimeTool, SearchWebTool -from services.llm_client import LLMClient -from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline -from utils.llm_provider import get_model - -API_V1_TEST_ROUTER = APIRouter(prefix="/api/v1/test", tags=["test"]) - - -class ResponseContent(BaseModel): - trending_ai_tool: str = Field( - description="The summary of the trending AI tool in about 50 words", - min_length=50, - max_length=100, - ) - current_date_time: str - - -@API_V1_TEST_ROUTER.get("") -async def test(): - client = LLMClient() - - response = await client._search_anthropic("Trending AI tool now") - # print(response) - - return {"data": ""} diff --git a/servers/fastapi/constants/llm.py b/servers/fastapi/constants/llm.py index ac4bd527..7d374f30 100644 --- a/servers/fastapi/constants/llm.py +++ b/servers/fastapi/constants/llm.py @@ -2,5 +2,5 @@ OPENAI_URL = "https://api.openai.com/v1" # Default models DEFAULT_OPENAI_MODEL = "gpt-4.1" -DEFAULT_GOOGLE_MODEL = "models/gemini-2.0-flash" -DEFAULT_ANTHROPIC_MODEL = "claude-3-5-sonnet-20240620" +DEFAULT_GOOGLE_MODEL = "models/gemini-2.5-flash" +DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514" diff --git a/servers/fastapi/models/user_config.py b/servers/fastapi/models/user_config.py index 50783544..c040d22c 100644 --- a/servers/fastapi/models/user_config.py +++ b/servers/fastapi/models/user_config.py @@ -35,3 +35,6 @@ class UserConfig(BaseModel): TOOL_CALLS: Optional[bool] = None DISABLE_THINKING: Optional[bool] = None EXTENDED_REASONING: Optional[bool] = None + + # Web Search + WEB_GROUNDING: Optional[bool] = None diff --git a/servers/fastapi/services/llm_client.py b/servers/fastapi/services/llm_client.py index e220f577..3e8b35f2 100644 --- a/servers/fastapi/services/llm_client.py +++ b/servers/fastapi/services/llm_client.py @@ -44,10 +44,11 @@ from utils.get_env import ( get_ollama_url_env, get_openai_api_key_env, get_tool_calls_env, + get_web_grounding_env, ) from utils.llm_provider import get_llm_provider, get_model from utils.parsers import parse_bool_or_none -from utils.schema_utils import ensure_strict_json_schema, flatten_json_schema +from utils.schema_utils import ensure_strict_json_schema class LLMClient: @@ -62,6 +63,15 @@ class LLMClient: return False return parse_bool_or_none(get_tool_calls_env()) or False + # ? Web Grounding + def enable_web_grounding(self) -> bool: + if ( + self.llm_provider == LLMProvider.OLLAMA + or self.llm_provider == LLMProvider.CUSTOM + ): + return False + return parse_bool_or_none(get_web_grounding_env()) or False + # ? Disable thinking def disable_thinking(self) -> bool: return parse_bool_or_none(get_disable_thinking_env()) or False @@ -569,7 +579,7 @@ class LLMClient: tools=google_tools, system_instruction=self._get_system_prompt(messages), response_mime_type="application/json" if not tools else None, - response_schema=response_format if not tools else None, + response_json_schema=response_format if not tools else None, max_output_tokens=max_tokens, ), ) diff --git a/servers/fastapi/services/llm_tool_calls_handler.py b/servers/fastapi/services/llm_tool_calls_handler.py index 1d8ffec4..ed0d51ee 100644 --- a/servers/fastapi/services/llm_tool_calls_handler.py +++ b/servers/fastapi/services/llm_tool_calls_handler.py @@ -51,7 +51,7 @@ class LLMToolCallsHandler: self.dynamic_tools.append(tool) match self.client.llm_provider: - case LLMProvider.OPENAI: + case LLMProvider.OPENAI | LLMProvider.OLLAMA | LLMProvider.CUSTOM: return self.parse_tool_openai(tool, strict) case LLMProvider.ANTHROPIC: return self.parse_tool_anthropic(tool) diff --git a/servers/fastapi/services/redis_service.py b/servers/fastapi/services/redis_service.py deleted file mode 100644 index f2e3d8c9..00000000 --- a/servers/fastapi/services/redis_service.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import Any, Optional -import redis -from redis.exceptions import RedisError - -from utils.get_env import ( - get_redis_db_env, - get_redis_host_env, - get_redis_password_env, - get_redis_port_env, -) - - -class RedisService: - def __init__(self): - self.redis_host = get_redis_host_env() or "localhost" - self.redis_port = int(get_redis_port_env() or "6379") - self.redis_db = int(get_redis_db_env() or "0") - self.redis_password = get_redis_password_env() or None - self.client = self._create_client() - - def _create_client(self) -> redis.Redis: - return redis.Redis( - host=self.redis_host, - port=self.redis_port, - db=self.redis_db, - password=self.redis_password, - decode_responses=True, - ) - - def set(self, key: str, value: Any, expire: Optional[int] = None) -> bool: - try: - return self.client.set(key, value, ex=expire) - except RedisError: - return False - - def get(self, key: str) -> Optional[str]: - try: - return self.client.get(key) - except RedisError: - return None - - def delete(self, key: str) -> bool: - try: - return bool(self.client.delete(key)) - except RedisError: - return False - - def exists(self, key: str) -> bool: - try: - return bool(self.client.exists(key)) - except RedisError: - return False - - def set_hash(self, name: str, mapping: dict) -> bool: - try: - return self.client.hmset(name, mapping) - except RedisError: - return False - - def get_hash(self, name: str) -> Optional[dict]: - try: - return self.client.hgetall(name) - except RedisError: - return None - - def delete_hash(self, name: str, *fields: str) -> int: - try: - return self.client.hdel(name, *fields) - except RedisError: - return 0 - - def set_list(self, name: str, values: list) -> bool: - try: - self.client.delete(name) - if values: - self.client.rpush(name, *values) - return True - except RedisError: - return False - - def get_list(self, name: str, start: int = 0, end: int = -1) -> Optional[list]: - try: - return self.client.lrange(name, start, end) - except RedisError: - return None - - def add_to_set(self, name: str, *values: str) -> int: - try: - return self.client.sadd(name, *values) - except RedisError: - return 0 - - def get_set(self, name: str) -> Optional[set]: - try: - return self.client.smembers(name) - except RedisError: - return None - - def remove_from_set(self, name: str, *values: str) -> int: - try: - return self.client.srem(name, *values) - except RedisError: - return 0 - - def clear(self) -> bool: - try: - return self.client.flushdb() - except RedisError: - return False - - def close(self): - try: - self.client.close() - except RedisError: - pass diff --git a/servers/fastapi/utils/get_env.py b/servers/fastapi/utils/get_env.py index c2c72efd..fa80b2a2 100644 --- a/servers/fastapi/utils/get_env.py +++ b/servers/fastapi/utils/get_env.py @@ -81,22 +81,6 @@ def get_pixabay_api_key_env(): return os.getenv("PIXABAY_API_KEY") -def get_redis_host_env(): - return os.getenv("REDIS_HOST") - - -def get_redis_port_env(): - return os.getenv("REDIS_PORT") - - -def get_redis_db_env(): - return os.getenv("REDIS_DB") - - -def get_redis_password_env(): - return os.getenv("REDIS_PASSWORD") - - def get_tool_calls_env(): return os.getenv("TOOL_CALLS") @@ -107,3 +91,7 @@ def get_disable_thinking_env(): def get_extended_reasoning_env(): return os.getenv("EXTENDED_REASONING") + + +def get_web_grounding_env(): + return os.getenv("WEB_GROUNDING") diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py index 6c0ad512..892b9cff 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py @@ -4,7 +4,10 @@ from models.llm_message import LLMSystemMessage, LLMUserMessage from models.llm_tools import GetCurrentDatetimeTool, SearchWebTool from services.llm_client import LLMClient from utils.get_dynamic_models import get_presentation_outline_model_with_n_slides +from utils.get_env import get_web_grounding_env from utils.llm_provider import get_model +from utils.parsers import parse_bool_or_none +from utils.user_config import get_user_config system_prompt = """ You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content. @@ -49,11 +52,13 @@ async def generate_ppt_outline( client = LLMClient() + tools = [SearchWebTool, GetCurrentDatetimeTool] + async for chunk in client.stream_structured( model, get_messages(prompt, n_slides, language, content), response_model.model_json_schema(), strict=True, - tools=[SearchWebTool, GetCurrentDatetimeTool], + tools=tools if client.enable_web_grounding() else None, ): yield chunk diff --git a/servers/fastapi/utils/set_env.py b/servers/fastapi/utils/set_env.py index 7ac0e335..ea3758f3 100644 --- a/servers/fastapi/utils/set_env.py +++ b/servers/fastapi/utils/set_env.py @@ -79,3 +79,7 @@ def set_disable_thinking_env(value): def set_extended_reasoning_env(value): os.environ["EXTENDED_REASONING"] = value + + +def set_web_grounding_env(value): + os.environ["WEB_GROUNDING"] = value \ No newline at end of file diff --git a/servers/fastapi/utils/user_config.py b/servers/fastapi/utils/user_config.py index 06235d5a..49fd1722 100644 --- a/servers/fastapi/utils/user_config.py +++ b/servers/fastapi/utils/user_config.py @@ -22,6 +22,7 @@ from utils.get_env import ( get_image_provider_env, get_pixabay_api_key_env, get_extended_reasoning_env, + get_web_grounding_env, ) from utils.parsers import parse_bool_or_none from utils.set_env import ( @@ -43,6 +44,7 @@ from utils.set_env import ( set_image_provider_env, set_pixabay_api_key_env, set_tool_calls_env, + set_web_grounding_env, ) @@ -76,12 +78,26 @@ def get_user_config(): IMAGE_PROVIDER=existing_config.IMAGE_PROVIDER or get_image_provider_env(), PIXABAY_API_KEY=existing_config.PIXABAY_API_KEY or get_pixabay_api_key_env(), PEXELS_API_KEY=existing_config.PEXELS_API_KEY or get_pexels_api_key_env(), - TOOL_CALLS=existing_config.TOOL_CALLS - or parse_bool_or_none(get_tool_calls_env()), - DISABLE_THINKING=existing_config.DISABLE_THINKING - or parse_bool_or_none(get_disable_thinking_env()), - EXTENDED_REASONING=existing_config.EXTENDED_REASONING - or parse_bool_or_none(get_extended_reasoning_env()), + TOOL_CALLS=( + existing_config.TOOL_CALLS + if existing_config.TOOL_CALLS is not None + else (parse_bool_or_none(get_tool_calls_env()) or False) + ), + DISABLE_THINKING=( + existing_config.DISABLE_THINKING + if existing_config.DISABLE_THINKING is not None + else (parse_bool_or_none(get_disable_thinking_env()) or False) + ), + EXTENDED_REASONING=( + existing_config.EXTENDED_REASONING + if existing_config.EXTENDED_REASONING is not None + else (parse_bool_or_none(get_extended_reasoning_env()) or False) + ), + WEB_GROUNDING=( + existing_config.WEB_GROUNDING + if existing_config.WEB_GROUNDING is not None + else (parse_bool_or_none(get_web_grounding_env()) or False) + ), ) @@ -122,5 +138,6 @@ def update_env_with_user_config(): if user_config.DISABLE_THINKING: set_disable_thinking_env(str(user_config.DISABLE_THINKING)) if user_config.EXTENDED_REASONING: - if user_config.EXTENDED_REASONING: - set_extended_reasoning_env(str(user_config.EXTENDED_REASONING)) + set_extended_reasoning_env(str(user_config.EXTENDED_REASONING)) + if user_config.WEB_GROUNDING: + set_web_grounding_env(str(user_config.WEB_GROUNDING)) diff --git a/servers/nextjs/app/api/user-config/route.ts b/servers/nextjs/app/api/user-config/route.ts index ff3c643a..03b801e3 100644 --- a/servers/nextjs/app/api/user-config/route.ts +++ b/servers/nextjs/app/api/user-config/route.ts @@ -57,6 +57,10 @@ export async function POST(request: Request) { userConfig.EXTENDED_REASONING === undefined ? existingConfig.EXTENDED_REASONING : userConfig.EXTENDED_REASONING, + WEB_GROUNDING: + userConfig.WEB_GROUNDING === undefined + ? existingConfig.WEB_GROUNDING + : userConfig.WEB_GROUNDING, USE_CUSTOM_URL: userConfig.USE_CUSTOM_URL === undefined ? existingConfig.USE_CUSTOM_URL diff --git a/servers/nextjs/components/AnthropicConfig.tsx b/servers/nextjs/components/AnthropicConfig.tsx index 567846b5..4b61bb65 100644 --- a/servers/nextjs/components/AnthropicConfig.tsx +++ b/servers/nextjs/components/AnthropicConfig.tsx @@ -19,6 +19,7 @@ interface AnthropicConfigProps { anthropicApiKey: string; anthropicModel: string; extendedReasoning: boolean; + webGrounding?: boolean; onInputChange: (value: string | boolean, field: string) => void; } @@ -27,6 +28,7 @@ export default function AnthropicConfig({ anthropicApiKey, anthropicModel, extendedReasoning, + webGrounding, onInputChange, }: AnthropicConfigProps) { const [openModelSelect, setOpenModelSelect] = useState(false); @@ -65,7 +67,7 @@ export default function AnthropicConfig({ const data = await response.json(); setAvailableModels(data); setModelsChecked(true); - onInputChange("claude-3-5-sonnet-20241022", "anthropic_model"); + onInputChange("claude-sonnet-4-20250514", "anthropic_model"); } else { console.error('Failed to fetch models'); setAvailableModels([]); @@ -226,6 +228,23 @@ export default function AnthropicConfig({ ) : null} + + {/* Web Grounding Toggle - at the end, below models dropdown */} +
+
+ + onInputChange(checked, "web_grounding")} + /> +
+

+ + If enabled, the model can use web search grounding when available. +

+
); } \ No newline at end of file diff --git a/servers/nextjs/components/GoogleConfig.tsx b/servers/nextjs/components/GoogleConfig.tsx index 6746f779..8d333dd3 100644 --- a/servers/nextjs/components/GoogleConfig.tsx +++ b/servers/nextjs/components/GoogleConfig.tsx @@ -13,16 +13,19 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { Switch } from "./ui/switch"; interface GoogleConfigProps { googleApiKey: string; googleModel: string; - onInputChange: (value: string, field: string) => void; + webGrounding?: boolean; + onInputChange: (value: string | boolean, field: string) => void; } export default function GoogleConfig({ googleApiKey, googleModel, + webGrounding, onInputChange }: GoogleConfigProps) { const [openModelSelect, setOpenModelSelect] = useState(false); @@ -61,7 +64,7 @@ export default function GoogleConfig({ const data = await response.json(); setAvailableModels(data); setModelsChecked(true); - onInputChange("models/gemini-2.0-flash", "google_model"); + onInputChange("models/gemini-2.5-flash", "google_model"); } else { console.error('Failed to fetch models'); setAvailableModels([]); @@ -205,6 +208,23 @@ export default function GoogleConfig({ ) : null} + + {/* Web Grounding Toggle - at the end, below models dropdown */} +
+
+ + onInputChange(checked, "web_grounding")} + /> +
+

+ + If enabled, the model can use web search grounding when available. +

+
); } \ No newline at end of file diff --git a/servers/nextjs/components/LLMSelection.tsx b/servers/nextjs/components/LLMSelection.tsx index 422e8333..ed308226 100644 --- a/servers/nextjs/components/LLMSelection.tsx +++ b/servers/nextjs/components/LLMSelection.tsx @@ -149,6 +149,7 @@ export default function LLMProviderSelection({ @@ -158,6 +159,7 @@ export default function LLMProviderSelection({ @@ -168,6 +170,7 @@ export default function LLMProviderSelection({ anthropicApiKey={llmConfig.ANTHROPIC_API_KEY || ""} anthropicModel={llmConfig.ANTHROPIC_MODEL || ""} extendedReasoning={llmConfig.EXTENDED_REASONING || false} + webGrounding={llmConfig.WEB_GROUNDING || false} onInputChange={input_field_changed} /> diff --git a/servers/nextjs/components/OpenAIConfig.tsx b/servers/nextjs/components/OpenAIConfig.tsx index b73695e9..7c465a99 100644 --- a/servers/nextjs/components/OpenAIConfig.tsx +++ b/servers/nextjs/components/OpenAIConfig.tsx @@ -13,16 +13,19 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { Switch } from "./ui/switch"; interface OpenAIConfigProps { openaiApiKey: string; openaiModel: string; - onInputChange: (value: string, field: string) => void; + webGrounding?: boolean; + onInputChange: (value: string | boolean, field: string) => void; } export default function OpenAIConfig({ openaiApiKey, openaiModel, + webGrounding, onInputChange }: OpenAIConfigProps) { const [openModelSelect, setOpenModelSelect] = useState(false); @@ -210,6 +213,23 @@ export default function OpenAIConfig({ ) : null} + + {/* Web Grounding Toggle - show at the end, below models dropdown */} +
+
+ + onInputChange(checked, "web_grounding")} + /> +
+

+ + If enabled, the model can use web search grounding when available. +

+
); } \ No newline at end of file diff --git a/servers/nextjs/types/llm_config.ts b/servers/nextjs/types/llm_config.ts index 0a44e639..5b73b215 100644 --- a/servers/nextjs/types/llm_config.ts +++ b/servers/nextjs/types/llm_config.ts @@ -31,6 +31,7 @@ export interface LLMConfig { TOOL_CALLS?: boolean; DISABLE_THINKING?: boolean; EXTENDED_REASONING?: boolean; + WEB_GROUNDING?: boolean; // Only used in UI settings USE_CUSTOM_URL?: boolean; diff --git a/servers/nextjs/utils/providerUtils.ts b/servers/nextjs/utils/providerUtils.ts index a0efb4f8..4c776ee8 100644 --- a/servers/nextjs/utils/providerUtils.ts +++ b/servers/nextjs/utils/providerUtils.ts @@ -48,6 +48,7 @@ export const updateLLMConfig = ( tool_calls: "TOOL_CALLS", disable_thinking: "DISABLE_THINKING", extended_reasoning: "EXTENDED_REASONING", + web_grounding: "WEB_GROUNDING", }; const configKey = fieldMappings[field]; diff --git a/start.js b/start.js index f2dfa1b0..2a0e336f 100644 --- a/start.js +++ b/start.js @@ -81,6 +81,7 @@ const setupUserConfigFromEnv = () => { TOOL_CALLS: process.env.TOOL_CALLS || existingConfig.TOOL_CALLS, DISABLE_THINKING: process.env.DISABLE_THINKING || existingConfig.DISABLE_THINKING, EXTENDED_REASONING: process.env.EXTENDED_REASONING || existingConfig.EXTENDED_REASONING, + WEB_GROUNDING: process.env.WEB_GROUNDING || existingConfig.WEB_GROUNDING, USE_CUSTOM_URL: process.env.USE_CUSTOM_URL || existingConfig.USE_CUSTOM_URL, };