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
15 changes: 15 additions & 0 deletions ai/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ LLM_API_KEY=
LLM_BASE_URL=https://factchat-cloud.mindlogic.ai/v1/gateway
LLM_PRO_MODEL=gemini-3.1-pro-preview
LLM_PRO_TEMPERATURE=0.2

# Github 관련
CORE_INTERNAL_BASE_URL=http://localhost:38010
CORE_INTERNAL_API_KEY=
CORE_INTERNAL_TIMEOUT_SEC=10

GITHUB_FALLBACK_TOKEN=
GITHUB_API_BASE_URL=https://api.github.com
REPO_MAX_SOURCE_FILES=8
REPO_MAX_SOURCE_FILE_BYTES=50000
REPO_FETCH_TIMEOUT_SEC=30

# 웹 이력서 관련
WEB_FETCH_TIMEOUT_SEC=20
WEB_MAX_HTML_BYTES=2000000
12 changes: 7 additions & 5 deletions ai/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ ai/

본 서버는 RabbitMQ **consumer**로 작동.

| Queue | Bind |
|-------|------|
| `q.ai.resume` | `ai.request.resume.*` |
| `q.ai.repo` | `ai.request.repo.*` |
| `q.ai.session` | `ai.request.session.*` |
| Queue | Routing key | 상태 |
|-------|-------------|------|
| `ai.analyze.resume` | `analyze.resume` | 본 구현 (PDF → MD) |
| `ai.analyze.repository` | `analyze.repository` | 본 구현 (GitHub README + tree + 소스 sampling) |
| `ai.analyze.web` | `analyze.web` | 본 구현 (URL → trafilatura) |
| `ai.generate.questions` | `generate.questions` | 큐만, 코드 미구현 |
| `ai.generate.followup` | `generate.followup` | 큐만, 코드 미구현 |

콜백 발행: `ai.callback.{type}` 익스체인지.
상세 envelope/스키마/재시도: [`/docs/messaging.md`](../docs/messaging.md).
Expand Down
1 change: 1 addition & 0 deletions ai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"boto3>=1.42.77",
"aiofiles>=24.1.0",
"pypdf>=5.1.0",
"trafilatura>=2.0.0",
"langchain>=1.2.13",
"langchain-core>=1.2.22",
"langchain-community>=0.4.1",
Expand Down
128 changes: 128 additions & 0 deletions ai/src/ai_server/analyzer/repository_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from __future__ import annotations

from dataclasses import dataclass

import structlog

from ai_server.analyzer.sources.github_repo import (
GitHubRepoSourceExtractor,
RepositoryFetchError,
)
from ai_server.chain.document_analysis_chain import DocumentAnalyzer
from ai_server.core.client import CoreClient, CoreTokenError
from ai_server.storage.base import ObjectStorage

log = structlog.get_logger(__name__)


class RepositoryAnalyzeError(Exception):
def __init__(self, *, code: str, message: str, retriable: bool) -> None:
super().__init__(message)
self.code = code
self.message = message
self.retriable = retriable


@dataclass(frozen=True)
class RepositoryAnalysisResult:
summary: str
tech_stack: list[str]
document_path: str


# 코어서버에서 토큰 받아오고 레포 가져온다
# 이후 LLM 분석하고 마크다운 저장
class RepositoryAnalyzer:
def __init__(
self,
*,
extractor: GitHubRepoSourceExtractor,
core_client: CoreClient,
chain: DocumentAnalyzer,
storage: ObjectStorage,
analyzed_key_template: str,
) -> None:
self._extractor = extractor
self._core_client = core_client
self._chain = chain
self._storage = storage
self._analyzed_key_template = analyzed_key_template

async def analyze(
self,
*,
repository_id: int,
repo_full_name: str,
default_branch: str = "main",
user_id: int | None,
) -> RepositoryAnalysisResult:
if user_id is None:
raise RepositoryAnalyzeError(
code="MISSING_USER_ID",
message="envelope.context.userId 없이는 GitHub token을 가져올 수 없음",
retriable=False,
)

log.info(
"repository.token.fetch",
user_id=user_id,
repository_id=repository_id,
)
try:
access_token = await self._core_client.fetch_github_token(user_id)
except CoreTokenError as err:
raise RepositoryAnalyzeError(
code=err.code,
message=err.message,
retriable=err.retriable,
) from err

log.info(
"repository.extract.start",
repository_id=repository_id,
repo_full_name=repo_full_name,
default_branch=default_branch,
)
try:
source = await self._extractor.extract(
repo_full_name,
access_token=access_token,
)
except RepositoryFetchError as err:
raise RepositoryAnalyzeError(
code=err.code,
message=err.message,
retriable=err.retriable,
) from err

if not source.text.strip():
raise RepositoryAnalyzeError(
code="EMPTY_REPO_CONTENT",
message="레포에서 추출된 텍스트가 비어 있음",
retriable=False,
)

log.info(
"repository.llm.start",
repository_id=repository_id,
text_chars=len(source.text),
)
analysis = await self._chain.analyze(
text=source.text,
source_type=source.source_type,
)

out_key = self._analyzed_key_template.format(repository_id=repository_id)
await self._storage.put_text(out_key, analysis.markdown)
log.info(
"repository.markdown.saved",
repository_id=repository_id,
key=out_key,
md_chars=len(analysis.markdown),
)

return RepositoryAnalysisResult(
summary=analysis.summary,
tech_stack=list(analysis.tech_stack),
document_path=out_key,
)
9 changes: 9 additions & 0 deletions ai/src/ai_server/analyzer/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
SourceExtractor,
SourceType,
)
from ai_server.analyzer.sources.github_repo import (
GitHubRepoSourceExtractor,
RepositoryFetchError,
)
from ai_server.analyzer.sources.pdf import PdfSourceExtractor
from ai_server.analyzer.sources.web import WebFetchError, WebSourceExtractor

__all__ = [
"ExtractedSource",
"SourceExtractor",
"SourceType",
"PdfSourceExtractor",
"GitHubRepoSourceExtractor",
"RepositoryFetchError",
"WebSourceExtractor",
"WebFetchError",
]
Loading