오류가 발생했습니다
+{error.message || "알 수 없는 오류가 발생했습니다."}
+ +diff --git a/.env.example b/.env.example
index a05f091..276bfb8 100644
--- a/.env.example
+++ b/.env.example
@@ -2,8 +2,8 @@
NEXT_PUBLIC_API_BASE=http://localhost:8000
# LLM (optional)
-OPENAI_API_KEY=your_openai_api_key_here
-OPENAI_EMBEDDING_MODEL=text-embedding-3-small
-OPENAI_CHAT_MODEL=gpt-4o-mini
-OPENAI_REQUEST_TIMEOUT=30
+GEMINI_API_KEY=your_gemini_api_key_here
+GEMINI_EMBEDDING_MODEL=text-embedding-004
+GEMINI_CHAT_MODEL=gemini-2.5-flash
+GEMINI_REQUEST_TIMEOUT=30
KNOWLEDGE_COPILOT_DATABASE_PATH=api/data/knowledge_copilot.db
diff --git a/api/src/config.py b/api/src/config.py
index 6397f0b..7bca72c 100644
--- a/api/src/config.py
+++ b/api/src/config.py
@@ -4,10 +4,10 @@
@dataclass(frozen=True)
class Settings:
- openai_api_key: str | None
+ gemini_api_key: str | None
embedding_model: str
chat_model: str
- openai_timeout: int
+ api_timeout: int
cors_origins: list[str]
db_path: str
@@ -20,10 +20,10 @@ def _parse_cors(origins: str) -> list[str]:
def load_settings() -> Settings:
return Settings(
- openai_api_key=os.getenv("OPENAI_API_KEY"),
- embedding_model=os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
- chat_model=os.getenv("OPENAI_CHAT_MODEL", "gpt-4o-mini"),
- openai_timeout=int(os.getenv("OPENAI_REQUEST_TIMEOUT", "30")),
+ gemini_api_key=os.getenv("GEMINI_API_KEY"),
+ embedding_model=os.getenv("GEMINI_EMBEDDING_MODEL", "text-embedding-004"),
+ chat_model=os.getenv("GEMINI_CHAT_MODEL", "gemini-2.5-flash"),
+ api_timeout=int(os.getenv("GEMINI_REQUEST_TIMEOUT", "30")),
cors_origins=_parse_cors(os.getenv("CORS_ORIGINS", "*")),
db_path=os.getenv(
"KNOWLEDGE_COPILOT_DATABASE_PATH",
diff --git a/api/src/services/rag.py b/api/src/services/rag.py
index d29b443..d1989ea 100644
--- a/api/src/services/rag.py
+++ b/api/src/services/rag.py
@@ -15,6 +15,7 @@ class LLMError(RuntimeError):
_EMBED_DIM = 256
+_GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta"
def _normalize_vector(vector: np.ndarray) -> np.ndarray:
@@ -54,26 +55,25 @@ def chunk_text(text: str, max_tokens: int = 220, overlap: int = 40) -> list[str]
async def embed_text(text: str) -> list[float]:
settings = load_settings()
- if not settings.openai_api_key:
+ if not settings.gemini_api_key:
return _local_embed(text)
- endpoint = "https://api.openai.com/v1/embeddings"
+ endpoint = f"{_GEMINI_BASE}/models/{settings.embedding_model}:embedContent"
headers = {
- "Authorization": f"Bearer {settings.openai_api_key}",
+ "x-goog-api-key": settings.gemini_api_key,
"Content-Type": "application/json",
}
payload = {
- "input": text,
- "model": settings.embedding_model,
+ "model": f"models/{settings.embedding_model}",
+ "content": {"parts": [{"text": text}]},
}
try:
- async with httpx.AsyncClient(timeout=settings.openai_timeout) as client:
+ async with httpx.AsyncClient(timeout=settings.api_timeout) as client:
response = await client.post(endpoint, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
- return data["data"][0]["embedding"]
+ return data["embedding"]["values"]
except Exception:
- # fallback to local mode if provider call is unavailable
return _local_embed(text)
@@ -112,7 +112,7 @@ def build_prompt(question: str, context_chunks: list[dict[str, Any]]) -> str:
async def generate_answer(question: str, context_chunks: list[dict[str, Any]], model: str | None = None) -> tuple[str, int, str]:
settings = load_settings()
- if not settings.openai_api_key:
+ if not settings.gemini_api_key:
if not context_chunks:
return (
"현재 API 키가 없어 데모 모드로 동작 중입니다. 먼저 참고 문서를 업로드하면 키워드 기반 응답을 제공합니다.",
@@ -127,29 +127,32 @@ async def generate_answer(question: str, context_chunks: list[dict[str, Any]], m
"local-fallback",
)
- endpoint = "https://api.openai.com/v1/chat/completions"
+ selected_model = model or settings.chat_model
+ endpoint = f"{_GEMINI_BASE}/models/{selected_model}:generateContent"
headers = {
- "Authorization": f"Bearer {settings.openai_api_key}",
+ "x-goog-api-key": settings.gemini_api_key,
"Content-Type": "application/json",
}
- selected_model = model or settings.chat_model
prompt = build_prompt(question, context_chunks)
payload = {
- "model": selected_model,
- "messages": [
- {"role": "system", "content": "You are a practical engineering assistant."},
- {"role": "user", "content": prompt},
+ "systemInstruction": {
+ "parts": [{"text": "You are a practical engineering assistant."}],
+ },
+ "contents": [
+ {"role": "user", "parts": [{"text": prompt}]},
],
- "temperature": 0.2,
+ "generationConfig": {
+ "temperature": 0.2,
+ },
}
try:
start = time.perf_counter()
- async with httpx.AsyncClient(timeout=settings.openai_timeout) as client:
+ async with httpx.AsyncClient(timeout=settings.api_timeout) as client:
response = await client.post(endpoint, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
- answer = data["choices"][0]["message"]["content"]
- used_tokens = int(data.get("usage", {}).get("total_tokens", 0))
+ answer = data["candidates"][0]["content"]["parts"][0]["text"]
+ used_tokens = int(data.get("usageMetadata", {}).get("totalTokenCount", 0))
_ = time.perf_counter() - start
return answer.strip(), used_tokens, selected_model
except Exception as err:
diff --git a/app/app/error.tsx b/app/app/error.tsx
new file mode 100644
index 0000000..1280588
--- /dev/null
+++ b/app/app/error.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+export default function ErrorBoundary({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+ {error.message || "알 수 없는 오류가 발생했습니다."}오류가 발생했습니다
+
{queryResult.answer}
-- 모델: {queryResult.model} / 지연: {queryResult.latency_ms}ms -
-{queryResult.citations.map((c) => `- [${c.document_id}] ${c.text}`).join("\n")}
+
-{`문서: ${metrics.documents}\n청크: ${metrics.chunks}\n질의: ${metrics.queries}\n평균 응답: ${metrics.avg_query_latency_ms}ms\n피드백: ${metrics.feedback_count}건 / 평점 ${metrics.avg_feedback_rating ?? "N/A"}`}
-
+