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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ==============================================================================
# FOWOCO AI Agent Server — Environment Variables (.env.example)
# ==============================================================================
# CI처럼 로컬 .env를 읽지 않아야 하는 실행은 FOWOCO_ENV_FILE=/dev/null 사용

# ------------------------------------------------------------------------------
# 1. Language Assistant — LLM 연동 설정 (OpenAI 호환)
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: ci

on:
pull_request:
branches: [develop, main]
workflow_dispatch: {}

permissions:
contents: read

concurrency:
group: ai-ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
PYTHONPATH: .
FOWOCO_ENV_FILE: /dev/null
FOWOCO_CLOVA_OCR_ENABLED: "false"
FOWOCO_INTENT_MODEL_ENABLED: "false"
FOWOCO_INTENT_ENABLE_AX: "false"
FOWOCO_INTENT_WARMUP_ON_START: "false"
FOWOCO_INTENT_WARMUP_REQUIRED: "false"

jobs:
core:
name: Core pytest · App Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: astral-sh/setup-uv@v6
with:
version: "0.12.5"
enable-cache: true

- name: Install core test dependencies
run: uv sync --frozen --extra dev

- name: Lint app and tests
run: uv run ruff check app tests

- name: Run deterministic core tests
run: >-
uv run pytest
-m "not qdrant_integration and not language_models and not rhwp_integration and not windows_ocr"

qdrant-contract:
name: Qdrant contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: astral-sh/setup-uv@v6
with:
version: "0.12.5"
enable-cache: true

- name: Install retrieval dependencies
run: uv sync --frozen --extra dev --extra language-retrieval

- name: Run Qdrant contract tests
run: >-
uv run pytest
tests/integration/language/test_qdrant_retrieval.py
tests/integration/language/test_runtime_composition.py
-m qdrant_integration

windows-ocr-contract:
name: Windows OCR script contract
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: astral-sh/setup-uv@v6
with:
version: "0.12.5"
enable-cache: true

- name: Install test dependencies
run: uv sync --frozen --extra dev

- name: Run PowerShell OCR contract tests
run: uv run pytest tests/ocr/test_smoke_script.py -m windows_ocr
5 changes: 4 additions & 1 deletion app/agents/intent/guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ def should_route_to_ax(
if activated_count >= self.max_trained_labels:
return RoutingResult(
should_route=True,
reason=f"활성 label {activated_count}개 (학습 최댓값 {self.max_trained_labels}개 이상)",
reason=(
f"활성 label {activated_count}개 "
f"(학습 최댓값 {self.max_trained_labels}개 이상)"
),
category="OOD_Label_Count",
)
if any(kw in clean_input for kw in self.status_kw):
Expand Down
7 changes: 6 additions & 1 deletion app/agents/workflow_graph/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ def to_public_status(status: str | None) -> str:

ALLOWED_TRANSITIONS: dict[TaskStatus, frozenset[TaskStatus]] = {
TaskStatus.DRAFT: frozenset(
{TaskStatus.NEEDS_INFO, TaskStatus.READY_FOR_REVIEW, TaskStatus.WAITING_WORKER, TaskStatus.CANCELLED}
{
TaskStatus.NEEDS_INFO,
TaskStatus.READY_FOR_REVIEW,
TaskStatus.WAITING_WORKER,
TaskStatus.CANCELLED,
}
),
TaskStatus.NEEDS_INFO: frozenset(
{
Expand Down
3 changes: 2 additions & 1 deletion app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import tempfile
from functools import lru_cache
from pathlib import Path
Expand All @@ -10,7 +11,7 @@
# 환경변수·.env에서 앱 설정을 읽어 오는 모델
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file=os.getenv("FOWOCO_ENV_FILE", ".env"),
env_file_encoding="utf-8",
env_prefix="FOWOCO_",
extra="ignore",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ markers = [
"qdrant_integration: mark test as requiring Qdrant integration",
"language_models: mark test as requiring language model cache",
"rhwp_integration: mark test as requiring the rhwp executable",
"windows_ocr: mark test as requiring PowerShell on Windows",
]


Expand Down
2 changes: 2 additions & 0 deletions tests/integration/language/test_qdrant_retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from app.agents.language.retrieval.models import ExpectedIndexContract, HybridVector
from app.agents.language.retrieval.qdrant_store import QdrantStore

pytestmark = pytest.mark.qdrant_integration


@pytest.fixture
def expected_contract() -> ExpectedIndexContract:
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/language/test_runtime_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def test_factory_selects_native_adapter_for_ollama_provider() -> None:
assert isinstance(generator, OllamaGenerationPort)


@pytest.mark.qdrant_integration
def test_factory_selects_hybrid_retriever_when_qdrant_is_configured() -> None:
from app.agents.language.composition import _build_production_ports

Expand All @@ -112,6 +113,7 @@ def test_factory_selects_hybrid_retriever_when_qdrant_is_configured() -> None:
assert isinstance(retriever, HybridEpsRetriever)


@pytest.mark.qdrant_integration
def test_factory_wires_fixed_revision_reranker_when_qdrant_is_configured(
tmp_path: Path,
) -> None:
Expand Down
10 changes: 10 additions & 0 deletions tests/ocr/test_smoke_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
from pathlib import Path
from uuid import UUID

import pytest

pytestmark = [
pytest.mark.windows_ocr,
pytest.mark.skipif(
shutil.which("powershell") is None and shutil.which("pwsh") is None,
reason="PowerShell is unavailable on this operating system",
),
]


def _powershell() -> str:
executable = shutil.which("powershell") or shutil.which("pwsh")
Expand Down
Loading