Skip to content

Commit 3e96614

Browse files
authored
Merge pull request #54 from kc3hack/frontend
Frontend
2 parents b15095c + 9ad473c commit 3e96614

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+10418
-429
lines changed

Backend/.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ __pycache__
33
.venv
44
.git
55
.gitignore
6+
.env
7+
.env.*
68
*.pyc
79
*.pyo
810
*.pyd

Backend/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
GEMINI_API_KEY=your_gemini_api_key
33

44
# Optional settings (defaults shown)
5-
GEMINI_MODEL=gemini-1.5-flash
6-
GEMINI_TIMEOUT_SECONDS=10
5+
GEMINI_MODEL=gemini-3-flash-preview
6+
GEMINI_TIMEOUT_SECONDS=30
77
GEMINI_PARALLELISM=5
88

99
# CockroachDB settings

Backend/Dockerfile

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,12 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
77
PIP_NO_CACHE_DIR=1 \
88
PIP_PREFER_BINARY=1
99

10-
RUN apt-get update \
11-
&& apt-get install -y --no-install-recommends \
12-
build-essential \
13-
rustc \
14-
cargo \
15-
&& rm -rf /var/lib/apt/lists/*
16-
1710
COPY requirements.txt ./
1811
RUN pip install --upgrade pip \
1912
&& pip install -r requirements.txt
2013

2114
COPY . .
2215

23-
EXPOSE 8000
16+
EXPOSE 8080
2417

25-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
18+
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}"]

Backend/README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,35 @@ uvicorn main:app --reload
3232
## Docker で起動
3333

3434
ルートディレクトリ(`/Users/honmayuudai/MyHobby/hackson/KC3Hack2026`)で実行してください。
35-
ARM64 などで `sudachipy` の Linux wheel が無い場合でもビルドできるよう、Dockerfile には Rust ツールチェーンを含めています(初回ビルドは時間がかかる可能性があります)。
35+
`docker-compose.yml` では `Backend` をデフォルトで `linux/amd64` で起動し、依存の wheel を優先利用します。
36+
必要に応じて `BACKEND_PLATFORM=linux/arm64` のように上書き可能です。
3637

3738
```bash
3839
make up-backend
3940
```
4041

42+
初回や Dockerfile 更新後に再ビルドしたい場合:
43+
44+
```bash
45+
make up-backend-build
46+
```
47+
48+
Docker の build cache が溜まって容量不足になった場合:
49+
50+
```bash
51+
make docker-clean
52+
```
53+
4154
- API ベース URL: `http://localhost:8000`
4255
- Swagger UI: `http://localhost:8000/docs`
4356
- ReDoc: `http://localhost:8000/redoc`
4457

58+
## Cloud Run デプロイ
59+
60+
Cloud Run へのデプロイ手順は以下を参照してください。
61+
62+
- `/Users/honmayuudai/MyHobby/hackson/KC3Hack2026/doc/cloud-run-deploy-workflow.md`
63+
4564
## 現在の API
4665

4766
- `GET /`
@@ -114,7 +133,7 @@ make up-backend
114133

115134
- `POST /dictionary/lookup`
116135
- 用語の意味を日本語で1〜2文の概要として返す
117-
- 現在は単語DB未実装のため、常にGeminiで生成する
136+
- このエンドポイントは現在DB参照未連携のため、常にGeminiで生成する
118137
- `terms` 指定時は、単語ごとに Gemini を非同期並列で呼び出す
119138
- リクエスト例(単体):
120139
```json
@@ -123,6 +142,22 @@ make up-backend
123142
"context": "LLMの会話で出てきた用語"
124143
}
125144
```
145+
146+
- `GET /dictionary/entries`
147+
- 辞書DBに登録されている単語一覧を取得する
148+
- クエリパラメータ: `q`(部分一致), `limit`, `offset`
149+
150+
- `POST /dictionary/entries/bulk`
151+
- カンマ/空白区切りの単語を一括登録する
152+
- 各単語について Gemini で説明を生成し、意味ベクトルを付与してDBに保存する
153+
- 既存単語はスキップされる
154+
155+
- `PATCH /dictionary/entries/{id}`
156+
- 辞書エントリの単語/説明を更新する
157+
- 単語変更時は意味ベクトルも再生成する
158+
159+
- `DELETE /dictionary/entries/{id}`
160+
- 辞書エントリを削除する
126161
- リクエスト例(複数):
127162
```json
128163
{

Backend/app/api/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import fastapi
2-
from app.api.endpoints import analysis, dictionary, hoge
2+
from app.api.endpoints import hoge, analysis, dictionary
33

44
router = fastapi.APIRouter()
55

6+
router.include_router(hoge.router, prefix="/hoge", tags=["hoge"])
67
router.include_router(analysis.router, prefix="/analysis", tags=["analysis"])
78
router.include_router(dictionary.router, prefix="/dictionary", tags=["dictionary"])
8-
router.include_router(hoge.router, prefix="/hoge", tags=["hoge"])
99

1010
# 新しくエンドポイントを追加するときは、
1111
# 1. app/api/endpoints/new_endpoint.pyを作成する

Backend/app/api/endpoints/dictionary.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,67 @@
11
from __future__ import annotations
22

3+
import re
4+
35
import fastapi
6+
from sqlalchemy.exc import IntegrityError
47

8+
from app.core.database import db
9+
from app.crud.dictionary import (
10+
create_dictionary,
11+
delete_dictionary,
12+
list_dictionaries,
13+
read_dictionary_by_id,
14+
read_dictionary_by_term,
15+
update_dictionary,
16+
)
517
from app.schemas.dictionary import (
18+
DictionaryBulkRegisterRequest,
19+
DictionaryBulkRegisterResponse,
20+
DictionaryBulkRegisterResult,
21+
DictionaryBulkRegisterSkipped,
22+
DictionaryEntryListResponse,
23+
DictionaryEntryResponse,
24+
DictionaryEntryUpdateRequest,
625
DictionaryLookupBatchResponse,
726
DictionaryLookupRequest,
827
DictionaryLookupResponse,
928
)
1029
from app.services.dictionary import lookup_term_summary, lookup_terms_summaries
30+
from app.services.text_analysis import vectorize_pretokenized_words
1131

1232
router = fastapi.APIRouter()
33+
_TERM_SPLIT_RE = re.compile(r"[,\s、,]+")
34+
35+
36+
def _ensure_db_available() -> None:
37+
if not db.is_available:
38+
raise fastapi.HTTPException(
39+
status_code=503,
40+
detail="DATABASE_URL is not configured",
41+
)
42+
43+
44+
def _to_entry_response(entry) -> DictionaryEntryResponse:
45+
return DictionaryEntryResponse(
46+
id=entry.id,
47+
term=entry.term,
48+
description=entry.description,
49+
meaning_vector=entry.meaning_vector,
50+
created_at=entry.created_at,
51+
updated_at=entry.updated_at,
52+
)
53+
54+
55+
def _parse_raw_terms(raw_terms: str) -> list[str]:
56+
terms = [term.strip() for term in _TERM_SPLIT_RE.split(raw_terms) if term.strip()]
57+
unique_terms = list(dict.fromkeys(terms))
58+
for term in unique_terms:
59+
if len(term) > 128:
60+
raise fastapi.HTTPException(
61+
status_code=422,
62+
detail="each term must be at most 128 characters",
63+
)
64+
return unique_terms
1365

1466

1567
# 辞書検索API本体。
@@ -37,3 +89,171 @@ def lookup(
3789
# 単体検索時は従来フォーマットのレスポンスを返す。
3890
result = lookup_term_summary(term=body.term or "", context=body.context)
3991
return DictionaryLookupResponse(**result)
92+
93+
94+
@router.get(
95+
"/entries",
96+
response_model=DictionaryEntryListResponse,
97+
summary="辞書エントリ一覧を取得する",
98+
)
99+
def list_entries(
100+
q: str | None = fastapi.Query(default=None, description="用語の部分一致検索"),
101+
limit: int = fastapi.Query(default=100, ge=1, le=200),
102+
offset: int = fastapi.Query(default=0, ge=0),
103+
) -> DictionaryEntryListResponse:
104+
_ensure_db_available()
105+
normalized_q = q.strip() if isinstance(q, str) and q.strip() else None
106+
rows, total = list_dictionaries(term_query=normalized_q, limit=limit, offset=offset)
107+
return DictionaryEntryListResponse(
108+
items=[_to_entry_response(row) for row in rows],
109+
total=total,
110+
limit=limit,
111+
offset=offset,
112+
)
113+
114+
115+
@router.patch(
116+
"/entries/{entry_id}",
117+
response_model=DictionaryEntryResponse,
118+
summary="辞書エントリを更新する",
119+
)
120+
def patch_entry(
121+
entry_id: int,
122+
body: DictionaryEntryUpdateRequest,
123+
) -> DictionaryEntryResponse:
124+
_ensure_db_available()
125+
current = read_dictionary_by_id(entry_id)
126+
if current is None:
127+
raise fastapi.HTTPException(status_code=404, detail="entry not found")
128+
129+
meaning_vector: list[float] | None = None
130+
if body.term is not None and body.term != current.term:
131+
duplicate = read_dictionary_by_term(body.term)
132+
if duplicate is not None and duplicate.id != entry_id:
133+
raise fastapi.HTTPException(
134+
status_code=409,
135+
detail="term already exists",
136+
)
137+
vectors = vectorize_pretokenized_words([(body.term,)])
138+
meaning_vector = vectors[0] if vectors else []
139+
140+
updated = update_dictionary(
141+
id=entry_id,
142+
term=body.term,
143+
description=body.description,
144+
meaning_vector=meaning_vector,
145+
)
146+
if updated is None:
147+
raise fastapi.HTTPException(status_code=404, detail="entry not found")
148+
return _to_entry_response(updated)
149+
150+
151+
@router.delete(
152+
"/entries/{entry_id}",
153+
status_code=204,
154+
summary="辞書エントリを削除する",
155+
)
156+
def remove_entry(entry_id: int) -> fastapi.Response:
157+
_ensure_db_available()
158+
deleted = delete_dictionary(entry_id)
159+
if not deleted:
160+
raise fastapi.HTTPException(status_code=404, detail="entry not found")
161+
return fastapi.Response(status_code=204)
162+
163+
164+
@router.post(
165+
"/entries/bulk",
166+
response_model=DictionaryBulkRegisterResponse,
167+
summary="用語を一括登録する",
168+
description=(
169+
"カンマまたは空白区切りの用語を受け取り、"
170+
"Gemini で説明文を生成して辞書DBに登録します。"
171+
),
172+
)
173+
def bulk_register(
174+
body: DictionaryBulkRegisterRequest,
175+
) -> DictionaryBulkRegisterResponse:
176+
_ensure_db_available()
177+
terms = _parse_raw_terms(body.raw_terms)
178+
if not terms:
179+
raise fastapi.HTTPException(status_code=422, detail="no terms found")
180+
181+
results: list[DictionaryBulkRegisterResult] = []
182+
created_count = 0
183+
skipped_count = 0
184+
185+
for term in terms:
186+
existing = read_dictionary_by_term(term)
187+
if existing is not None:
188+
skipped_count += 1
189+
results.append(
190+
DictionaryBulkRegisterResult(
191+
term=term,
192+
status="skipped",
193+
skipped=DictionaryBulkRegisterSkipped(
194+
term=term,
195+
reason="already exists",
196+
),
197+
)
198+
)
199+
continue
200+
201+
try:
202+
llm_result = lookup_term_summary(term=term)
203+
except fastapi.HTTPException as exc:
204+
skipped_count += 1
205+
results.append(
206+
DictionaryBulkRegisterResult(
207+
term=term,
208+
status="skipped",
209+
skipped=DictionaryBulkRegisterSkipped(
210+
term=term,
211+
reason=f"lookup failed ({exc.status_code})",
212+
),
213+
)
214+
)
215+
continue
216+
217+
vectors = vectorize_pretokenized_words([(term,)])
218+
meaning_vector = vectors[0] if vectors else []
219+
220+
try:
221+
entry_id = create_dictionary(
222+
term=term,
223+
description=llm_result["summary"],
224+
meaning_vector=meaning_vector,
225+
)
226+
created_entry = read_dictionary_by_id(entry_id)
227+
if created_entry is None:
228+
raise fastapi.HTTPException(
229+
status_code=500,
230+
detail="failed to load created entry",
231+
)
232+
created_count += 1
233+
results.append(
234+
DictionaryBulkRegisterResult(
235+
term=term,
236+
status="created",
237+
entry=_to_entry_response(created_entry),
238+
)
239+
)
240+
except IntegrityError:
241+
# 並列処理などで同時に同一語が作成された場合はスキップ扱いにする。
242+
skipped_count += 1
243+
results.append(
244+
DictionaryBulkRegisterResult(
245+
term=term,
246+
status="skipped",
247+
skipped=DictionaryBulkRegisterSkipped(
248+
term=term,
249+
reason="already exists",
250+
),
251+
)
252+
)
253+
254+
return DictionaryBulkRegisterResponse(
255+
requested_count=len(terms),
256+
created_count=created_count,
257+
skipped_count=skipped_count,
258+
results=results,
259+
)

0 commit comments

Comments
 (0)