Migrate Google backend from deprecated google-generativeai to google-genai SDK with grounding support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-02-24 04:44:44 +00:00
commit 6e2d956be6
7 changed files with 45 additions and 58 deletions

View file

@ -58,6 +58,7 @@ llm:
You are a helpful assistant on a Meshtastic mesh network. You are a helpful assistant on a Meshtastic mesh network.
Keep responses VERY brief - under 250 characters total. Keep responses VERY brief - under 250 characters total.
Be concise but friendly. No markdown formatting. Be concise but friendly. No markdown formatting.
google_grounding: false # Enable Google Search grounding (Gemini only, $35/1k queries)
# === WEATHER === # === WEATHER ===
weather: weather:

View file

@ -59,6 +59,7 @@ llm:
You are a helpful assistant on a Meshtastic mesh network. You are a helpful assistant on a Meshtastic mesh network.
Keep responses VERY brief - under 250 characters total. Keep responses VERY brief - under 250 characters total.
Be concise but friendly. No markdown formatting. Be concise but friendly. No markdown formatting.
google_grounding: false
EOF EOF
echo "Default config created. Configure via http://localhost:7682" echo "Default config created. Configure via http://localhost:7682"
fi fi

View file

@ -1,9 +1,10 @@
"""Google Gemini LLM backend with rolling summary memory.""" """Google Gemini LLM backend with rolling summary memory and Google Search grounding."""
import logging import logging
from typing import Optional from typing import Optional
import google.generativeai as genai from google import genai
from google.genai import types
from ..config import LLMConfig from ..config import LLMConfig
from ..memory import RollingSummaryMemory from ..memory import RollingSummaryMemory
@ -23,7 +24,7 @@ Summary (2-3 sentences):"""
class GoogleBackend(LLMBackend): class GoogleBackend(LLMBackend):
"""Google Gemini backend with rolling summary memory.""" """Google Gemini backend with rolling summary memory and optional grounding."""
def __init__( def __init__(
self, self,
@ -32,19 +33,9 @@ class GoogleBackend(LLMBackend):
window_size: int = 4, window_size: int = 4,
summarize_threshold: int = 8, summarize_threshold: int = 8,
): ):
"""Initialize Google backend.
Args:
config: LLM configuration
api_key: Google API key
window_size: Recent message pairs to keep in full
summarize_threshold: Messages before re-summarizing
"""
self.config = config self.config = config
genai.configure(api_key=api_key) self._client = genai.Client(api_key=api_key)
self._model = genai.GenerativeModel(config.model)
# Initialize rolling summary memory with Gemini summarize function
self._memory = RollingSummaryMemory( self._memory = RollingSummaryMemory(
summarize_fn=self._summarize_messages, summarize_fn=self._summarize_messages,
window_size=window_size, window_size=window_size,
@ -52,7 +43,7 @@ class GoogleBackend(LLMBackend):
) )
async def _summarize_messages(self, messages: list[dict]) -> str: async def _summarize_messages(self, messages: list[dict]) -> str:
"""Summarize messages using Google Gemini API.""" """Summarize messages using Gemini."""
if not messages: if not messages:
return "No previous conversation." return "No previous conversation."
@ -62,9 +53,10 @@ class GoogleBackend(LLMBackend):
prompt = _SUMMARIZE_PROMPT.format(conversation=conversation) prompt = _SUMMARIZE_PROMPT.format(conversation=conversation)
try: try:
response = await self._model.generate_content_async( response = await self._client.aio.models.generate_content(
prompt, model=self.config.model,
generation_config=genai.types.GenerationConfig( contents=prompt,
config=types.GenerateContentConfig(
max_output_tokens=150, max_output_tokens=150,
temperature=0.3, temperature=0.3,
), ),
@ -81,18 +73,7 @@ class GoogleBackend(LLMBackend):
max_tokens: int = 300, max_tokens: int = 300,
user_id: Optional[str] = None, user_id: Optional[str] = None,
) -> str: ) -> str:
"""Generate a response using Google Gemini API. """Generate a response using Google Gemini with optional grounding."""
Args:
messages: Conversation history
system_prompt: System prompt
max_tokens: Maximum tokens to generate
user_id: User identifier (enables memory optimization)
Returns:
Generated response
"""
# Use memory manager to optimize context if user_id provided
enhanced_system = system_prompt enhanced_system = system_prompt
final_messages = messages final_messages = messages
@ -101,43 +82,40 @@ class GoogleBackend(LLMBackend):
user_id=user_id, user_id=user_id,
full_history=messages, full_history=messages,
) )
if summary: if summary:
enhanced_system = f"{system_prompt}\n\nPrevious conversation summary: {summary}" enhanced_system = f"{system_prompt}\n\nPrevious conversation summary: {summary}"
final_messages = recent_messages final_messages = recent_messages
logger.debug( logger.debug(
f"Using summary + {len(recent_messages)} recent messages " f"Using summary + {len(recent_messages)} recent messages "
f"(total history: {len(messages)})" f"(total history: {len(messages)})"
) )
try: try:
# Create model with system instruction for persistent system prompt contents = []
model = genai.GenerativeModel( for msg in final_messages:
self.config.model, role = "model" if msg["role"] == "assistant" else "user"
contents.append(
types.Content(
role=role,
parts=[types.Part.from_text(text=msg["content"])],
)
)
tools = []
if self.config.google_grounding:
tools.append(types.Tool(google_search=types.GoogleSearch()))
config = types.GenerateContentConfig(
system_instruction=enhanced_system if enhanced_system else None, system_instruction=enhanced_system if enhanced_system else None,
max_output_tokens=max_tokens,
temperature=0.7,
tools=tools if tools else None,
) )
# Convert messages to Gemini format response = await self._client.aio.models.generate_content(
# Gemini uses "user" and "model" roles model=self.config.model,
history = [] contents=contents,
for msg in final_messages[:-1]: # All but last message config=config,
role = "model" if msg["role"] == "assistant" else "user"
history.append({"role": role, "parts": [msg["content"]]})
# Start chat with history
chat = model.start_chat(history=history)
# Get the last user message
last_message = final_messages[-1]["content"] if final_messages else ""
# Generate response
response = await chat.send_message_async(
last_message,
generation_config=genai.types.GenerationConfig(
max_output_tokens=max_tokens,
temperature=0.7,
),
) )
return response.text.strip() if response.text else "" return response.text.strip() if response.text else ""
@ -147,9 +125,7 @@ class GoogleBackend(LLMBackend):
raise raise
def get_memory(self) -> RollingSummaryMemory: def get_memory(self) -> RollingSummaryMemory:
"""Get the memory manager instance."""
return self._memory return self._memory
async def close(self) -> None: async def close(self) -> None:
"""Clean up - nothing to close for Google client."""
pass pass

View file

@ -261,6 +261,7 @@ class Configurator:
table.add_row("5", "System Prompt", f"[dim]{len(self.config.llm.system_prompt)} chars[/dim]") table.add_row("5", "System Prompt", f"[dim]{len(self.config.llm.system_prompt)} chars[/dim]")
table.add_row("6", "Use System Prompt", self._status_icon(self.config.llm.use_system_prompt)) table.add_row("6", "Use System Prompt", self._status_icon(self.config.llm.use_system_prompt))
table.add_row("7", "Web Search", self._status_icon(self.config.llm.web_search)) table.add_row("7", "Web Search", self._status_icon(self.config.llm.web_search))
table.add_row("8", "Google Grounding", self._status_icon(self.config.llm.google_grounding))
table.add_row("0", "Back", "") table.add_row("0", "Back", "")
console.print(table) console.print(table)
@ -311,6 +312,13 @@ class Configurator:
elif choice == 7: elif choice == 7:
self.config.llm.web_search = not self.config.llm.web_search self.config.llm.web_search = not self.config.llm.web_search
self.modified = True self.modified = True
elif choice == 8:
if self.config.llm.backend == "google":
self.config.llm.google_grounding = not self.config.llm.google_grounding
self.modified = True
else:
console.print("[yellow]Google grounding is only available with the google backend.[/yellow]")
input("Press Enter to continue...")
def _weather_settings(self) -> None: def _weather_settings(self) -> None:
"""Weather settings submenu.""" """Weather settings submenu."""

View file

@ -90,6 +90,7 @@ class LLMConfig:
) )
use_system_prompt: bool = True # Toggle to disable sending system prompt use_system_prompt: bool = True # Toggle to disable sending system prompt
web_search: bool = False # Enable web search (Open WebUI feature) web_search: bool = False # Enable web search (Open WebUI feature)
google_grounding: bool = False # Enable Google Search grounding (Gemini only)
@dataclass @dataclass

View file

@ -33,7 +33,7 @@ dependencies = [
"aiosqlite>=0.19.0", "aiosqlite>=0.19.0",
"openai>=1.0.0", "openai>=1.0.0",
"anthropic>=0.18.0", "anthropic>=0.18.0",
"google-generativeai>=0.4.0", "google-genai>=1.0.0",
"rich>=13.0.0", "rich>=13.0.0",
"httpx>=0.25.0", "httpx>=0.25.0",
] ]

View file

@ -3,6 +3,6 @@ pyyaml>=6.0
aiosqlite>=0.19.0 aiosqlite>=0.19.0
openai>=1.0.0 openai>=1.0.0
anthropic>=0.18.0 anthropic>=0.18.0
google-generativeai>=0.4.0 google-genai>=1.0.0
rich>=13.0.0 rich>=13.0.0
httpx>=0.25.0 httpx>=0.25.0