Compare commits

..

No commits in common. "8793650c2f222c33cb92e7c2b54413462de22381" and "507b495b90d0f0f9a87150fb032bde28d860e5f2" have entirely different histories.

2 changed files with 133 additions and 145 deletions

View File

@ -737,7 +737,6 @@ async def event_snapshot(
): ):
event_complete = False event_complete = False
jpg_bytes = None jpg_bytes = None
frame_time = 0
try: try:
event = Event.get(Event.id == event_id, Event.end_time != None) event = Event.get(Event.id == event_id, Event.end_time != None)
event_complete = True event_complete = True
@ -791,7 +790,7 @@ async def event_snapshot(
headers = { headers = {
"Content-Type": "image/jpeg", "Content-Type": "image/jpeg",
"Cache-Control": "private, max-age=31536000" if event_complete else "no-store", "Cache-Control": "private, max-age=31536000" if event_complete else "no-store",
"X-Frame-Time": str(frame_time), "X-Frame-Time": frame_time,
} }
if params.download: if params.download:

View File

@ -1,5 +1,6 @@
"""Gemini Provider for Frigate AI.""" """Gemini Provider for Frigate AI."""
import json
import logging import logging
from typing import Any, Optional from typing import Any, Optional
@ -83,169 +84,147 @@ class GeminiClient(GenAIClient):
tools: Optional[list[dict[str, Any]]] = None, tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto", tool_choice: Optional[str] = "auto",
) -> dict[str, Any]: ) -> dict[str, Any]:
"""
Send chat messages to Gemini with optional tool definitions.
Implements function calling/tool usage for Gemini models.
"""
try: try:
# Convert messages to Gemini format
gemini_messages = []
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
# Map roles to Gemini format
if role == "system":
# Gemini doesn't have system role, prepend to first user message
if gemini_messages and gemini_messages[0].role == "user":
gemini_messages[0].parts[
0
].text = f"{content}\n\n{gemini_messages[0].parts[0].text}"
else:
gemini_messages.append(
types.Content(
role="user", parts=[types.Part.from_text(text=content)]
)
)
elif role == "assistant":
gemini_messages.append(
types.Content(
role="model", parts=[types.Part.from_text(text=content)]
)
)
elif role == "tool":
# Handle tool response
function_response = {
"name": msg.get("name", ""),
"response": content,
}
gemini_messages.append(
types.Content(
role="function",
parts=[
types.Part.from_function_response(function_response)
],
)
)
else: # user
gemini_messages.append(
types.Content(
role="user", parts=[types.Part.from_text(text=content)]
)
)
# Convert tools to Gemini format
gemini_tools = None
if tools: if tools:
gemini_tools = [] function_declarations = []
for tool in tools: for tool in tools:
if tool.get("type") == "function": if tool.get("type") == "function":
func = tool.get("function", {}) func_def = tool.get("function", {})
gemini_tools.append( function_declarations.append(
types.Tool( genai.protos.FunctionDeclaration(
function_declarations=[ name=func_def.get("name"),
types.FunctionDeclaration( description=func_def.get("description"),
name=func.get("name", ""), parameters=genai.protos.Schema(
description=func.get("description", ""), type=genai.protos.Type.OBJECT,
parameters=func.get("parameters", {}), properties={
prop_name: genai.protos.Schema(
type=_convert_json_type_to_gemini(
prop.get("type")
),
description=prop.get("description"),
) )
] for prop_name, prop in func_def.get(
"parameters", {}
)
.get("properties", {})
.items()
},
required=func_def.get("parameters", {}).get(
"required", []
),
),
) )
) )
# Configure tool choice tool_config = genai.protos.Tool(
tool_config = None function_declarations=function_declarations
if tool_choice:
if tool_choice == "none":
tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode="NONE")
) )
elif tool_choice == "auto":
tool_config = types.ToolConfig( if tool_choice == "none":
function_calling_config=types.FunctionCallingConfig(mode="AUTO") function_calling_config = genai.protos.FunctionCallingConfig(
mode=genai.protos.FunctionCallingConfig.Mode.NONE
) )
elif tool_choice == "required": elif tool_choice == "required":
tool_config = types.ToolConfig( function_calling_config = genai.protos.FunctionCallingConfig(
function_calling_config=types.FunctionCallingConfig(mode="ANY") mode=genai.protos.FunctionCallingConfig.Mode.ANY
) )
else:
# Build request config function_calling_config = genai.protos.FunctionCallingConfig(
config_params = {"candidate_count": 1} mode=genai.protos.FunctionCallingConfig.Mode.AUTO
if gemini_tools:
config_params["tools"] = gemini_tools
if tool_config:
config_params["tool_config"] = tool_config
# Merge runtime_options
if isinstance(self.genai_config.runtime_options, dict):
config_params.update(self.genai_config.runtime_options)
response = self.provider.models.generate_content(
model=self.genai_config.model,
contents=gemini_messages,
config=types.GenerateContentConfig(**config_params),
) )
else:
tool_config = None
function_calling_config = None
# Check if response is valid contents = []
if not response or not response.candidates: for msg in messages:
return { role = msg.get("role")
"content": None, content = msg.get("content", "")
"tool_calls": None,
"finish_reason": "error", if role == "system":
continue
elif role == "user":
contents.append({"role": "user", "parts": [content]})
elif role == "assistant":
parts = [content] if content else []
if "tool_calls" in msg:
for tc in msg["tool_calls"]:
parts.append(
genai.protos.FunctionCall(
name=tc["function"]["name"],
args=json.loads(tc["function"]["arguments"]),
)
)
contents.append({"role": "model", "parts": parts})
elif role == "tool":
tool_name = msg.get("name", "")
tool_result = (
json.loads(content) if isinstance(content, str) else content
)
contents.append(
{
"role": "function",
"parts": [
genai.protos.FunctionResponse(
name=tool_name,
response=tool_result,
)
],
} }
)
generation_config = genai.types.GenerationConfig(
candidate_count=1,
)
if function_calling_config:
generation_config.function_calling_config = function_calling_config
response = self.provider.generate_content(
contents,
tools=[tool_config] if tool_config else None,
generation_config=generation_config,
request_options=genai.types.RequestOptions(timeout=self.timeout),
)
candidate = response.candidates[0]
content = None content = None
tool_calls = None tool_calls = None
# Extract content and tool calls from response if response.candidates and response.candidates[0].content:
if candidate.content and candidate.content.parts: parts = response.candidates[0].content.parts
for part in candidate.content.parts: text_parts = [p.text for p in parts if hasattr(p, "text") and p.text]
if part.text: if text_parts:
content = part.text.strip() content = " ".join(text_parts).strip()
elif part.function_call:
# Handle function call function_calls = [
if tool_calls is None: p.function_call
for p in parts
if hasattr(p, "function_call") and p.function_call
]
if function_calls:
tool_calls = [] tool_calls = []
for fc in function_calls:
try:
arguments = (
dict(part.function_call.args)
if part.function_call.args
else {}
)
except Exception:
arguments = {}
tool_calls.append( tool_calls.append(
{ {
"id": part.function_call.name or "", "id": f"call_{hash(fc.name)}",
"name": part.function_call.name or "", "name": fc.name,
"arguments": arguments, "arguments": dict(fc.args)
if hasattr(fc, "args")
else {},
} }
) )
# Determine finish reason
finish_reason = "error" finish_reason = "error"
if hasattr(candidate, "finish_reason") and candidate.finish_reason: if response.candidates:
from google.genai.types import FinishReason finish_reason_map = {
genai.types.FinishReason.STOP: "stop",
if candidate.finish_reason == FinishReason.STOP: genai.types.FinishReason.MAX_TOKENS: "length",
finish_reason = "stop" genai.types.FinishReason.SAFETY: "stop",
elif candidate.finish_reason == FinishReason.MAX_TOKENS: genai.types.FinishReason.RECITATION: "stop",
finish_reason = "length" genai.types.FinishReason.OTHER: "error",
elif candidate.finish_reason in [ }
FinishReason.SAFETY, finish_reason = finish_reason_map.get(
FinishReason.RECITATION, response.candidates[0].finish_reason, "error"
]: )
finish_reason = "error"
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
elif tool_calls: elif tool_calls:
finish_reason = "tool_calls" finish_reason = "tool_calls"
elif content: elif content:
@ -257,19 +236,29 @@ class GeminiClient(GenAIClient):
"finish_reason": finish_reason, "finish_reason": finish_reason,
} }
except errors.APIError as e: except GoogleAPICallError as e:
logger.warning("Gemini API error during chat_with_tools: %s", str(e)) logger.warning("Gemini returned an error: %s", str(e))
return { return {
"content": None, "content": None,
"tool_calls": None, "tool_calls": None,
"finish_reason": "error", "finish_reason": "error",
} }
except Exception as e: except Exception as e:
logger.warning( logger.warning("Unexpected error in Gemini chat_with_tools: %s", str(e))
"Gemini returned an error during chat_with_tools: %s", str(e)
)
return { return {
"content": None, "content": None,
"tool_calls": None, "tool_calls": None,
"finish_reason": "error", "finish_reason": "error",
} }
def _convert_json_type_to_gemini(json_type: str) -> genai.protos.Type:
type_map = {
"string": genai.protos.Type.STRING,
"integer": genai.protos.Type.INTEGER,
"number": genai.protos.Type.NUMBER,
"boolean": genai.protos.Type.BOOLEAN,
"array": genai.protos.Type.ARRAY,
"object": genai.protos.Type.OBJECT,
}
return type_map.get(json_type, genai.protos.Type.STRING)