Skip to content

Commit 7258a51

Browse files
authored
Merge branch 'main' into feat/kms-integration
2 parents 3c187a5 + 4cf9986 commit 7258a51

20 files changed

Lines changed: 1042 additions & 17 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""add google_aistudio value to the providertype enum
2+
3+
Revision ID: 072
4+
Revises: 071
5+
Create Date: 2026-07-07 00:00:00.000000
6+
7+
Stored value is the member NAME 'google_aistudio'
8+
"""
9+
10+
from alembic import op
11+
12+
revision = "072"
13+
down_revision = "071"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade() -> None:
19+
with op.get_context().autocommit_block():
20+
op.execute("ALTER TYPE providertype ADD VALUE IF NOT EXISTS 'google_aistudio'")
21+
22+
23+
def downgrade() -> None:
24+
# Postgres cannot drop a value from an enum type; downgrade is a no-op.
25+
pass

‎backend/app/crud/evaluations/batch.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def build_google_evaluation_jsonl(
201201
Each line:
202202
{
203203
"key": <dataset_item_id>,
204-
"request": { contents, systemInstruction?, generationConfig? }
204+
"request": { contents, systemInstruction?, generationConfig?, tools? }
205205
}
206206
"""
207207
jsonl_data: list[dict[str, Any]] = []
@@ -218,6 +218,11 @@ def build_google_evaluation_jsonl(
218218
"thinkingLevel": reasoning,
219219
}
220220

221+
tools: list[dict[str, Any]] = []
222+
knowledge_base_ids = google_params.get("knowledge_base_ids")
223+
if knowledge_base_ids:
224+
tools.append({"fileSearch": {"fileSearchStoreNames": knowledge_base_ids}})
225+
221226
for item in dataset_items:
222227
question = item["input"].get("question", "")
223228
if not question:
@@ -233,6 +238,8 @@ def build_google_evaluation_jsonl(
233238
request["systemInstruction"] = {"parts": [{"text": system_instruction}]}
234239
if generation_config:
235240
request["generationConfig"] = generation_config
241+
if tools:
242+
request["tools"] = tools
236243

237244
jsonl_data.append({"key": item["id"], "request": request})
238245

‎backend/app/crud/rag/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .gemini import GeminiCrud, GeminiFileSearchStoreCrud
12
from .open_ai import OpenAICrud, OpenAIFileCrud, OpenAIVectorStoreCrud

‎backend/app/crud/rag/gemini.py‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import logging
2+
import time
3+
from typing import NoReturn
4+
5+
from google import genai
6+
from google.genai import errors, types
7+
8+
logger = logging.getLogger(__name__)
9+
10+
# Import into a File Search store is a long-running operation. Poll until it
11+
# reports done, bounded so a stuck operation surfaces as an error instead of
12+
# blocking the collection task indefinitely.
13+
IMPORT_POLL_INTERVAL_SECONDS = 5
14+
IMPORT_POLL_TIMEOUT_SECONDS = 300
15+
16+
17+
def _raise_genai_error(
18+
err: errors.APIError,
19+
*,
20+
log_prefix: str,
21+
resource: str,
22+
) -> NoReturn:
23+
"""Translate a google-genai SDK error into a user-facing InterruptedError.
24+
25+
Mirrors the source-tagged, fault-based pattern in
26+
OpenAIVectorStoreCrud.update and GoogleAIProvider.execute: 4xx is the
27+
caller's fault (warning), 5xx is provider-side (error).
28+
"""
29+
if isinstance(err, errors.ClientError):
30+
code = err.code
31+
status = err.status or ""
32+
msg = err.message or str(err)
33+
if code == 429:
34+
error_message = (
35+
f"[GEMINI] Rate limit / quota exceeded (code: 429 {status}): "
36+
f"{msg}. Wait at least 1 minute and retry; if the issue "
37+
f"persists, request a quota increase from Google or contact Kaapi."
38+
)
39+
elif code == 403:
40+
error_message = (
41+
f"[GEMINI] Authentication / permission denied (code: 403 "
42+
f"{status}): {msg}. Verify the Gemini API key is valid, not "
43+
f"expired, and has access to the File Search API for this project."
44+
)
45+
elif code == 404:
46+
error_message = (
47+
f"[GEMINI] Resource not found (code: 404 {status}): {msg}. "
48+
f"Verify the File Search store and file names exist and have "
49+
f"not expired."
50+
)
51+
elif code == 400:
52+
error_message = (
53+
f"[GEMINI] Bad request (code: 400 {status}): {msg}. Review the "
54+
f"File Search store configuration and file references — the "
55+
f"request shape may be invalid."
56+
)
57+
else:
58+
error_message = (
59+
f"[GEMINI] Client error (code: {code} {status}): {msg}. Review "
60+
f"the request configuration; if the issue persists, contact Kaapi."
61+
)
62+
logger.warning(
63+
f"[{log_prefix}] {error_message} | resource={resource}",
64+
exc_info=True,
65+
)
66+
else:
67+
status = getattr(err, "status", "") or ""
68+
code = getattr(err, "code", "unknown")
69+
msg = getattr(err, "message", None) or str(err)
70+
error_message = (
71+
f"[GEMINI] Server error (code: {code} {status}): {msg}. This is "
72+
f"typically transient (Gemini overloaded or internal error) — retry "
73+
f"in a few seconds. If the issue persists, contact Kaapi."
74+
)
75+
logger.error(
76+
f"[{log_prefix}] {error_message} | resource={resource}",
77+
exc_info=True,
78+
)
79+
raise InterruptedError(error_message)
80+
81+
82+
class GeminiCrud:
83+
def __init__(self, client: genai.Client) -> None:
84+
if client is None:
85+
logger.error("[GeminiCrud] Gemini client is not configured")
86+
raise ValueError("Gemini client is not configured")
87+
88+
self.client = client
89+
90+
91+
class GeminiFileSearchStoreCrud(GeminiCrud):
92+
"""CRUD for Google AI Studio (Gemini) File Search stores."""
93+
94+
def create(self) -> str:
95+
logger.info("[GeminiFileSearchStoreCrud.create] Creating file search store")
96+
try:
97+
store = self.client.file_search_stores.create(
98+
config=types.CreateFileSearchStoreConfig()
99+
)
100+
except (errors.ClientError, errors.ServerError) as err:
101+
_raise_genai_error(
102+
err,
103+
log_prefix="GeminiFileSearchStoreCrud.create",
104+
resource="<new store>",
105+
)
106+
107+
logger.info(
108+
f"[GeminiFileSearchStoreCrud.create] File search store created | "
109+
f"store_name={store.name}"
110+
)
111+
return store.name
112+
113+
def import_document(self, store_name: str, file_name: str) -> None:
114+
logger.info(
115+
f"[GeminiFileSearchStoreCrud.import_document] Importing file into store | "
116+
f"store_name={store_name}, file_name={file_name}"
117+
)
118+
try:
119+
operation = self.client.file_search_stores.import_file(
120+
file_search_store_name=store_name,
121+
file_name=file_name,
122+
)
123+
except (errors.ClientError, errors.ServerError) as err:
124+
_raise_genai_error(
125+
err,
126+
log_prefix="GeminiFileSearchStoreCrud.import_document",
127+
resource=store_name,
128+
)
129+
130+
deadline = time.monotonic() + IMPORT_POLL_TIMEOUT_SECONDS
131+
while not operation.done:
132+
if time.monotonic() > deadline:
133+
error_message = (
134+
f"[KAAPI] Timed out (code: import-timeout) after "
135+
f"{IMPORT_POLL_TIMEOUT_SECONDS}s waiting for Gemini to import "
136+
f"'{file_name}' into store '{store_name}'. Retry the import, "
137+
f"or use fewer / smaller files per collection."
138+
)
139+
logger.error(
140+
f"[GeminiFileSearchStoreCrud.import_document] {error_message}"
141+
)
142+
raise InterruptedError(error_message)
143+
144+
time.sleep(IMPORT_POLL_INTERVAL_SECONDS)
145+
try:
146+
operation = self.client.operations.get(operation)
147+
except (errors.ClientError, errors.ServerError) as err:
148+
_raise_genai_error(
149+
err,
150+
log_prefix="GeminiFileSearchStoreCrud.import_document",
151+
resource=store_name,
152+
)
153+
154+
if operation.error:
155+
error_message = (
156+
f"[GEMINI] Import operation failed for '{file_name}' into store "
157+
f"'{store_name}': {operation.error}. Verify the file is a "
158+
f"supported format and has not expired, then retry."
159+
)
160+
logger.error(f"[GeminiFileSearchStoreCrud.import_document] {error_message}")
161+
raise InterruptedError(error_message)
162+
163+
logger.info(
164+
f"[GeminiFileSearchStoreCrud.import_document] File imported | "
165+
f"store_name={store_name}, file_name={file_name}"
166+
)
167+
168+
def delete(self, store_name: str) -> None:
169+
logger.info(
170+
f"[GeminiFileSearchStoreCrud.delete] Deleting file search store | "
171+
f"store_name={store_name}"
172+
)
173+
try:
174+
self.client.file_search_stores.delete(
175+
name=store_name,
176+
config=types.DeleteFileSearchStoreConfig(force=True),
177+
)
178+
except (errors.ClientError, errors.ServerError) as err:
179+
_raise_genai_error(
180+
err,
181+
log_prefix="GeminiFileSearchStoreCrud.delete",
182+
resource=store_name,
183+
)
184+
185+
logger.info(
186+
f"[GeminiFileSearchStoreCrud.delete] File search store deleted | "
187+
f"store_name={store_name}"
188+
)

‎backend/app/models/collection.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ class ProviderType(str, Enum):
1616
"""Supported LLM providers for collections."""
1717

1818
openai = "openai"
19+
google_aistudio = "google-aistudio"
1920
# BEDROCK = "bedrock"
20-
# GEMINI = "gemini"
2121

2222

2323
class Collection(SQLModel, table=True):
@@ -123,7 +123,7 @@ class CallbackRequest(SQLModel):
123123
class ProviderOptions(SQLModel):
124124
"""LLM provider configuration."""
125125

126-
provider: Literal["openai"] = Field(
126+
provider: Literal["openai", "google-aistudio"] = Field(
127127
default="openai", description="LLM provider to use for this collection"
128128
)
129129

‎backend/app/models/llm/constants.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Provider(StrEnum):
2727
Provider.ELEVENLABS,
2828
Provider.GOOGLE_AISTUDIO,
2929
]
30-
RAGProvider = Literal[Provider.OPENAI]
30+
RAGProvider = Literal[Provider.OPENAI, Provider.GOOGLE_AISTUDIO]
3131

3232
KaapiProvider = Literal[
3333
Provider.OPENAI,

‎backend/app/services/collections/helpers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def get_service_name(provider: str) -> str:
2525
"""Get the collection service name for a provider."""
2626
names = {
2727
"openai": "openai vector store",
28+
"google-aistudio": "gemini file search store",
2829
# "bedrock": "bedrock knowledge base",
29-
# "gemini": "gemini file search store",
3030
}
3131
return names.get(provider.lower(), "")
3232

‎backend/app/services/collections/providers/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from app.services.collections.providers.base import BaseProvider
2+
from app.services.collections.providers.gemini import GeminiAIStudioProvider
23
from app.services.collections.providers.openai import OpenAIProvider
34
from app.services.collections.providers.registry import (
45
LLMProvider,

0 commit comments

Comments
 (0)