Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ uv run pytest
* **Frontend**: Next.js 16 (React 19), TypeScript, Tailwind CSS v4
* **Backend**: Python 3.12, FastAPI, Uvicorn, LangGraph, LangChain, Alembic
* **Database**: PostgreSQL 17 (pgvector), Neo4j 5 Graph DB
* **AI & Data**: NVIDIA NIM (`openai/gpt-oss-120b`, `BAAI/bge-m3`), Tavily, yfinance, 청약홈 공공데이터 API
* **AI & Data**: NVIDIA NIM (`google/gemma-4-31b-it`, `BAAI/bge-m3`), Tavily, yfinance, 청약홈 공공데이터 API
* **Infra**: 오라클 VM(systemd + Caddy/Let's Encrypt) · Vercel · GitHub Actions
* **Tooling**: uv, ruff, pytest, ESLint

Expand Down
17 changes: 17 additions & 0 deletions backend/app/api/tax_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pathlib import Path

from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from pydantic import BaseModel

from backend.app.api.uploads import read_upload_capped
from backend.app.services.tax.rate_diff import diff_against_current
Expand Down Expand Up @@ -74,6 +75,22 @@ def _pdf_to_text(raw: bytes) -> str:
) from exc


class ExtractTextRequest(BaseModel):
text: str
year: str = "2026"
use_llm: bool = True


@router.post("/extract")
def extract_text(req: ExtractTextRequest) -> dict:
"""개정안 원문 텍스트에서 세율을 추출해 현행과 비교·검증한다."""
text = req.text.strip()
if not text:
raise HTTPException(status_code=400, detail="텍스트가 비어 있습니다.")
proposed = extract_rate_set(text[:_MAX_TEXT_CHARS], year=req.year, use_llm=req.use_llm)
return _diff_payload(proposed)


@router.post("/extract/upload")
async def extract_upload(
file: UploadFile = File(...),
Expand Down
4 changes: 1 addition & 3 deletions backend/app/services/agent/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,5 @@ def build_chat_model(temperature: float = 0.7, max_tokens: int = 4000) -> NIMCha
max_tokens=max_tokens,
timeout=REQUEST_TIMEOUT,
max_retries=0, # 같은 키로 재시도해봐야 429는 그대로 — 위 루프가 키를 바꿔가며 재시도한다
# 구 모델(qwen3-next-80b)은 스트리밍이 delta 4개로만 쪼개져 오면서 비스트리밍보다
# 2.5배 느려 disable_streaming=True로 껐었다. gpt-oss-120b로 교체 후 실측하니
# 정상 토큰 단위로 흐르므로(93청크/700토큰) 다시 켠다.
# 정상 토큰 단위로 스트리밍되는 gemma-4-31b-it 모델 사용
)
18 changes: 15 additions & 3 deletions backend/app/services/agent/nodes/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@

tax_and_market_lookup을 골랐고 질문이 특정 자산 종류(예: 주식·채권·예금·부동산)에 한정되면,
asset_types에 해당 자산 종류명을 적으십시오(세법 조회를 그 자산으로 좁힘). 자산을 특정하지 않은
일반 질문이면 asset_types는 비워 두십시오(전체 세법 조회)."""
일반 질문이면 asset_types는 비워 두십시오(전체 세법 조회).

반드시 다음 JSON 형식(객체)으로만 응답하십시오:
{"tools": ["도구명1", "도구명2"], "asset_types": [], "ticker": ""}"""


# 데이터 검색이 필요함을 시사하는 토큰(짧은 메시지라도 이게 있으면 잡담이 아니다).
Expand Down Expand Up @@ -101,7 +104,14 @@ def _keyword_route(text: str) -> list[str]:
if any(k in text for k in ("계산", "얼마야", "얼마나 내", "비과세")):
route.append("tax_calculator")
# 사기 메시지 검증
if any(k in text for k in ("사기", "피싱", "스미싱", "믿어도 되", "괜찮을까", "의심")):
if any(
k in text
for k in (
"사기", "피싱", "스미싱", "믿어도 되", "괜찮을까", "의심",
"사고났어", "이체해줘", "선입금", "수수료 먼저", "입금하면",
"보증금", "환급금", "계좌정보를 입력", "액정 깨져",
)
):
route.append("fraud_check")
# 라이브 웹 리서치(현재 금리/상품·금리 동향·국세청 해석)
if any(k in text for k in ("예금", "적금", "연금저축", "국채", "상품", "가입", "이율", "우대")):
Expand Down Expand Up @@ -181,7 +191,9 @@ class _Route(BaseModel):
# 라우터 출력은 도구 이름 몇 개짜리 JSON이라 300토큰이면 충분하다. 기본값(4000)을
# 두면 모델이 structured output에서 폭주할 때 4000토큰을 다 태우고서야 끝난다
# (실측 45초). 상한을 낮추면 폭주해도 금방 끝나고 아래 _keyword_route로 폴백된다.
router = build_chat_model(temperature=0.0, max_tokens=300).with_structured_output(_Route)
router = build_chat_model(temperature=0.0, max_tokens=300).with_structured_output(
_Route, method="json_mode"
)
prompt_input = f"[대화 맥락]\n{dialogue_context}\n\n[현재 사용자 발화]\n{user_text}"
result = router.invoke(
[SystemMessage(content=_INTENT_PROMPT), HumanMessage(content=prompt_input)]
Expand Down
2 changes: 1 addition & 1 deletion docs/competition/proposal-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@

- **Frontend**: Next.js 16(App Router), TypeScript, Tailwind, GSAP/D3/WebGL 시각화
- **Backend**: FastAPI, LangGraph(StateGraph + PostgresSaver 체크포인터)
- **AI**: NVIDIA NIM(gpt-oss-120b / bge-m3 임베딩), Tavily·Naver 웹검색 도구
- **AI**: NVIDIA NIM(google/gemma-4-31b-it / bge-m3 임베딩), Tavily·Naver 웹검색 도구
- **Data**: PostgreSQL(pgvector) · Neo4j · yfinance · 청약홈 공공데이터
- **Infra**: Docker Compose, Alembic 마이그레이션, GitHub Actions CI
- 전체 다이어그램: `architecture.svg` 참조
Expand Down
67 changes: 47 additions & 20 deletions frontend/src/app/tax-rates/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
"use client";

import { useRef, useState } from "react";
import { ArrowRight, CheckCircle, Warning, FilePdf, ShieldCheck } from "@phosphor-icons/react";
import { ArrowRight, CheckCircle, Warning, FilePdf, ShieldCheck, Sparkle } from "@phosphor-icons/react";
import {
extractRates,
extractRatesUpload,
type RateDiffRow,
type RateExtractResult,
} from "@/lib/api";
import { Card, PageTitle, SectionLabel, fmtKRW } from "@/components/ui";

const SAMPLE_AMENDMENT = `2026년 귀속 세법개정안 요약
- 해외주식 양도소득세율을 20%에서 22%로 상향한다.
- 이자·배당 분리과세율은 15.4%로 유지한다.
- 금융소득종합과세 기준을 2,000만원에서 3,000만원으로 상향한다.`;

/** 세율값을 종류에 맞게 렌더 — rate는 퍼센트, amount는 원화. */
function fmtRateValue(kind: "rate" | "amount", value: number): string {
return kind === "rate" ? `${(value * 100).toFixed(2).replace(/\.?0+$/, "")}%` : fmtKRW(value);
Expand All @@ -33,12 +39,27 @@ function DiffRow({ row }: { row: RateDiffRow }) {
}

export default function TaxRatesPage() {
const [text, setText] = useState(SAMPLE_AMENDMENT);
const [year, setYear] = useState("2026");
const [result, setResult] = useState<RateExtractResult | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileRef = useRef<HTMLInputElement>(null);

async function onExtract() {
if (!text.trim()) return;
setBusy(true);
setError(null);
setResult(null);
try {
setResult(await extractRates(text, year, false));
} catch (e) {
setError(e instanceof Error ? e.message : "세율 추출에 실패했습니다.");
} finally {
setBusy(false);
}
}

async function onUpload(file: File) {
setBusy(true);
setError(null);
Expand All @@ -58,12 +79,19 @@ export default function TaxRatesPage() {
<PageTitle
eyebrow="결정론 계산기 · 개정 대응"
title="세율 개정안 인입 (미리보기)"
subtitle="개정안 PDF·파일을 올리면 시스템이 세율을 추출해 현행과 비교·검증합니다. 자동 반영은 하지 않습니다 — 세율은 코드 상수로만 결정되는 결정론 불변식을 지키기 위해서입니다. LLM은 추출만, 계산은 코드가 합니다."
subtitle="개정안 텍스트를 붙여넣거나 PDF·파일을 올리면 시스템이 세율을 추출해 현행과 비교·검증합니다. 자동 반영은 하지 않습니다 — 세율은 코드 상수로만 결정되는 결정론 불변식을 지키기 위해서입니다. LLM은 추출만, 계산은 코드가 합니다."
/>

{/* ── 입력 (파일 업로드) ── */}
{/* ── 입력 (텍스트 붙여넣기 + 파일 업로드) ── */}
<Card className="space-y-4">
<SectionLabel>개정안 파일 업로드</SectionLabel>
<SectionLabel>개정안 텍스트 입력</SectionLabel>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
rows={5}
className="field w-full resize-y p-3 font-mono-spec text-sm"
placeholder="세법개정안 텍스트를 붙여넣으세요…"
/>
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm text-muted">
귀속연도
Expand All @@ -74,6 +102,17 @@ export default function TaxRatesPage() {
/>
</label>

<button
onClick={onExtract}
disabled={busy || !text.trim()}
className="btn-accent inline-flex items-center gap-2 disabled:opacity-50"
>
<Sparkle size={16} weight="fill" />
{busy ? "추출 중…" : "세율 추출"}
</button>

<span className="text-xs text-muted">또는</span>

<input
ref={fileRef}
type="file"
Expand All @@ -87,27 +126,15 @@ export default function TaxRatesPage() {
<button
onClick={() => fileRef.current?.click()}
disabled={busy}
className="btn-accent inline-flex items-center gap-2 disabled:opacity-50"
className="btn-ghost inline-flex items-center gap-2 disabled:opacity-50"
>
<FilePdf size={16} />
{busy ? "추출 중…" : "개정안 파일 업로드"}
개정안 파일 업로드
</button>
</div>
<p className="text-xs text-muted">
PDF·TXT·MD 지원. PDF는 서버가 표까지 텍스트로 추출해 세율을 뽑고 현행과 비교합니다.
별도 텍스트 작성 없이 개정안 문서 그대로 올리면 됩니다.
</p>
<p className="text-xs text-muted">
세법개정안 원문은{" "}
<a
href="https://www.moef.go.kr"
target="_blank"
rel="noopener noreferrer"
className="text-accent underline decoration-accent/40 underline-offset-2 hover:decoration-accent"
>
기획재정부(moef.go.kr) ↗
</a>
에서 받아 그대로 올리세요.
위 기본 예시로 1초 만에 테스트하거나, PDF·TXT·MD 파일을 직접 올릴 수 있습니다.
PDF는 서버가 표까지 텍스트로 추출해 세율을 뽑고 현행과 비교합니다.
</p>
</Card>

Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,14 @@ export function getCurrentRates(year = "2026"): Promise<RateCurrent> {
return apiGet(`/api/v1/tax-rates/current?year=${encodeURIComponent(year)}`);
}

export function extractRates(
text: string,
year = "2026",
useLlm = false,
): Promise<RateExtractResult> {
return apiPost("/api/v1/tax-rates/extract", { text, year, use_llm: useLlm });
}

/** 개정안 파일(PDF·TXT·MD)을 업로드해 추출한다. PDF는 서버가 파서로 텍스트를 뽑아 동일 파이프라인을 탄다. */
export async function extractRatesUpload(
file: File,
Expand Down
4 changes: 2 additions & 2 deletions openwiki/.last-update.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"updatedAt": "2026-08-30T14:24:28.855Z",
"updatedAt": "2026-09-02T14:22:01.117Z",
"command": "update",
"gitHead": "3916b2f5a44de605bf2e604c7cd04cd7c459a2fd",
"gitHead": "8f27862e3e7e1d239b8010b65fea71e06f913907",
"model": "openai/gpt-oss-120b"
}
44 changes: 17 additions & 27 deletions openwiki/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This repository exposes a **FastAPI** server under the `/api/v1` prefix. The API

* **Path Prefix** – All routes start with `/api/v1`.
* **Response Models** – Pydantic models are used for request validation and response schemas (see the source files).
* **Rate Limiting** – Global IP‑based request throttling is enforced by `backend/app/middleware/rate_limit.py`. See the middleware source for configuration (`RATE_LIMIT`, `RATE_LIMIT_WINDOW`).
* **Authentication** – Auth routes are available (`/api/v1/auth/login`). The UI can obtain a JWT token via login; if `AUTH_ENABLED` is false, the auth layer is bypassed and `user_uuid` may be passed directly.
* **Streaming** – The chat endpoint supports Server‑Sent Events (SSE) for token‑by‑token streaming.

Expand Down Expand Up @@ -53,18 +54,6 @@ This repository exposes a **FastAPI** server under the `/api/v1` prefix. The API
---

## Cheongyak (Housing) Endpoints (`backend/app/api/cheongyak.py`)

## Tax Rates API (`backend/app/api/tax_rates.py`)

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/tax-rates/extract` | Extract tax rate proposals from raw text (up to 20k chars). Returns diff against current rates and validation issues. |
| `POST` | `/tax-rates/extract/upload` | Upload a `.txt`, `.md`, or `.pdf` file to extract tax rates. Supports same diff and validation as above. |
| `POST` | `/tax-rates/apply` | Apply a validated tax rate proposal to the overlay for a given year. Returns confirmation and active rates payload. |
| `GET` | `/tax-rates/current` | Retrieve the current effective tax rates for a given year (default 2026), after any overlays. |
| `GET` | `/tax-rates/current?year=2025` | Retrieve rates for a specific year.

These endpoints support the tax calculator tool and UI tax‑rate management features. Source: `backend/app/api/tax_rates.py`.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/cheongyak/list/{kind}` | List housing projects of a given `kind` (e.g., `apt`, `officetel`).
Expand All @@ -75,28 +64,29 @@ These endpoints support the tax calculator tool and UI tax‑rate management fea

---

## Finetuning & Embedding Pipelines (`backend/app/api/finetune.py`)
## Tax Rates API (`backend/app/api/tax_rates.py`)
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/finetune/upload` | Upload a document (PDF/TXT/MD/JSONL) for embedding and triplet generation.
| `POST` | `/finetune/jobs` | Start a new finetuning pipeline job. Returns a job ID.
| `POST` | `/finetune/train/jobs` | Start LoRA finetuning training job on generated triplet dataset. Returns a job ID.
| `GET` | `/finetune/jobs` | List all finetuning jobs with status.
| `GET` | `/finetune/jobs/{id}` | Get detailed progress and logs for a specific job.
| `GET` | `/finetune/datasets` | Preview generated training/evaluation datasets (triplets) for a given sub‑directory.
| `POST` | `/tax-rates/extract` | Extract tax rate proposals from raw text (up to 20k chars). Returns diff against current rates and validation issues. |
| `POST` | `/tax-rates/extract/upload` | Upload a `.txt`, `.md`, or `.pdf` file to extract tax rates. Supports same diff and validation as above. |
| `POST` | `/tax-rates/apply` | Apply a validated tax rate proposal to the overlay for a given year. Returns confirmation and active rates payload. |
| `GET` | `/tax-rates/current` | Retrieve the current effective tax rates for a given year (default 2026), after any overlays. |
| `GET` | `/tax-rates/current?year=2025` | Retrieve rates for a specific year. |

---

## Knowledge Graph Endpoints (`backend/app/api/graph.py`)
## Knowledge Graph & Document RAG Endpoints (`backend/app/api/graph.py`)
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/graph/build/jobs` | Trigger an incremental Neo4j graph build job.
| `GET` | `/graph/build/jobs` | List active/completed graph‑build jobs.
| `GET` | `/graph/build/jobs/{id}` | Retrieve job status and logs.
| `GET` | `/graph/snapshot` | Return a JSON snapshot of Neo4j nodes/edges (used by the UI visualiser).
| `GET` | `/graph/documents` | List currently ingested RAG documents (used by Knowledge Panel).
| `POST` | `/graph/ingest/jobs` | Trigger a document ingest job (parse & embed uploaded file).
| `GET` | `/graph/ingest/jobs/{job_id}` | Get status of a document ingest job.
| `GET` | `/graph/documents` | List currently ingested RAG documents (used by Knowledge Panel). |
| `DELETE` | `/graph/documents/{source}` | Delete a document from RAG (`emb_passages` records and `data/raw_documents/` file). |
| `POST` | `/graph/upload` | Upload a financial document to `data/raw_documents/` for embedding. |
| `POST` | `/graph/ingest/jobs` | Trigger a document ingest job (parse & embed uploaded file). |
| `GET` | `/graph/ingest/jobs/{job_id}` | Get status of a document ingest job. |
| `POST` | `/graph/build/jobs` | Trigger an incremental Neo4j graph build job. |
| `GET` | `/graph/build/jobs` | List active/completed graph‑build jobs. |
| `GET` | `/graph/build/jobs/{id}` | Retrieve job status and logs. |
| `GET` | `/graph/snapshot` | Return a JSON snapshot of Neo4j nodes/edges (used by the UI visualiser). |


---
Expand Down
2 changes: 1 addition & 1 deletion openwiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Midas Touch is organized around **four major technical domains** that align with

## Backend Architecture

* **Entry point** – `backend/app/main.py` creates a FastAPI app, registers eight routers (`chat`, `users`, `finetune`, `graph`, `query`, `stocks`, `cheongyak`, `research`). It also starts a background validation loop that periodically scores the analysis memory.
* **Entry point** – `backend/app/main.py` creates a FastAPI app, registers routers (`auth`, `chat`, `users`, `tax_rates`, `graph`, `query`, `stocks`, `cheongyak`, `research`). It also starts a background validation loop that periodically scores the analysis memory.
* **API Routers** – each router lives under `backend/app/api/` and groups related endpoints (e.g., `stocks.py` exposes quick‑analysis, backtest, grid‑search, and memory statistics). Routes are mounted under `/api/v1`.
* **Services** – business logic is encapsulated in `backend/app/services/`:
* `agent/` – LangGraph state graph, node definitions, tool implementations, persistence via `PostgresSaver`.
Expand Down
Loading
Loading