Skip to content

feat(ai): 매물 추천 Text-to-SQL 및 LLM 기반 의도 분류 구현 (#29) - #42

Merged
crolvlee merged 7 commits into
developfrom
feat/29-chat-property-search
Jun 23, 2026
Merged

feat(ai): 매물 추천 Text-to-SQL 및 LLM 기반 의도 분류 구현 (#29)#42
crolvlee merged 7 commits into
developfrom
feat/29-chat-property-search

Conversation

@crolvlee

@crolvlee crolvlee commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

변경 내용

  • 의도 분류를 키워드 순차 매칭에서 LLM structured output 기반으로 전환
  • RouteDecision Pydantic 스키마 추가 (intent Literal 6종 + reasoning)
  • LLMClient.classify() 메서드 추가 (GMS OpenAI-compatible API 호출)
  • LLM 호출 실패 시 classify_intent_fallback()Intent.FALLBACK 반환
  • Intent.GENERAL_CHAT 추가 및 그래프 라우팅 연결
  • SupabaseVectorClient.search_properties() 추가 (Text-to-SQL 매물 조회)
  • SpringClient 의존성 주입 구조 개선 (timeout_seconds, http_get 주입 가능)
  • _similarity_search_legal_documents_rest() REST fallback 추가

연결 이슈

closes #29

테스트

  • 로컬 동작 확인 ("강남구 오피스텔 가장 싼거 추천해줘"PROPERTY_SEARCH, 매물 20개 반환)

리뷰 포인트

classify_intent_llm()은 LLM 실패 시 키워드 fallback 없이 FALLBACK intent를 반환합니다. 기존 키워드 기반 분류를 완전히 제거한 의도적인 결정이며, 서비스 가용성보다 분류 정확도를 우선한 트레이드오프입니다.

search_properties()에서 정렬 기준이 created_at DESC로 고정되어 있어, "가장 싼거" 같은 정렬 의도가 반영되지 않습니다. 별도 이슈로 처리 예정입니다.

Summary by CodeRabbit

  • New Features
    • Extended AI intent recognition from 4 to 6 types, including general chat handling.
    • Improved property search by deriving search criteria with the AI and querying listings directly.
  • Bug Fixes
    • Ensured general chat inputs are routed correctly (falls back when needed).
  • Configuration
    • Updated the AI backend provider to GMS API (OpenAI-compatible).
  • Documentation
    • Refreshed architecture and API spec to reflect the new intent categories and updated property field naming.
  • Tests
    • Added and updated unit tests for LLM-based intent classification and fallback behavior.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7f92fbc-2a5a-49fa-a650-6f45319c0d3d

📥 Commits

Reviewing files that changed from the base of the PR and between f46d915 and 63c6d69.

📒 Files selected for processing (3)
  • backend-ai/app/clients/llm_client.py
  • backend-ai/app/graph/nodes/property_search.py
  • docs/08_API_SPEC.md

📝 Walkthrough

Walkthrough

Replaces keyword-based intent classification with an LLM-driven RouteDecision Pydantic model, adds a GENERAL_CHAT intent to the enum and routing graph, and replaces SpringClient property search with LLM criteria extraction (extract_property_criteria) feeding directly into a new SupabaseVectorClient.search_properties method. Configuration, tests, and documentation are updated accordingly.

Changes

LLM Intent Classification and Text-to-SQL Property Search

Layer / File(s) Summary
Intent enum and config baseline
backend-ai/app/graph/state.py, backend-ai/app/core/config.py
Adds GENERAL_CHAT to the Intent enum and changes llm_base_url default from the OpenAI API URL to an empty string.
LLMClient prompt templates and classify/extract methods
backend-ai/app/clients/llm_client.py
Adds json import, CLASSIFY_INTENT_PROMPT and PROPERTY_CRITERIA_PROMPT constants, and implements classify and extract_property_criteria methods that POST to /chat/completions, parse JSON responses, and return typed fallbacks on error.
RouteDecision schema and LLM-based classify_intent node
backend-ai/app/graph/nodes/classify_intent.py, backend-ai/app/graph/builder.py, backend-ai/tests/test_agent_chat.py, backend-ai/tests/test_classify_intent.py
Replaces keyword-based classify_message with a RouteDecision Pydantic model, classify_intent_fallback, and classify_intent_llm; updates the state node and graph router for GENERAL_CHAT; adds and rewrites unit tests for schema validation, happy-path parameterization, and fallback cases.
LLM criteria extraction and Supabase direct property search
backend-ai/app/clients/supabase_client.py, backend-ai/app/clients/spring_client.py, backend-ai/app/graph/nodes/property_search.py
Adds SupabaseVectorClient.search_properties with dynamic parameterized filtering and statement timeout; strips the SpringClient stub down to an empty response; rewires the property_search node to extract criteria via LLMClient then query Supabase directly.
Architecture, ADR, and API spec documentation
AGENTS.md, CLAUDE.md, docs/02_ARCHITECTURE.md, docs/03_ADR.md, docs/08_API_SPEC.md
Updates AI backend label to GMS API, expands intent count from 4 to 6, adds ADR-012 (LLM classification with Pydantic validation) and ADR-013 (Text-to-SQL search), and adds the intent value table and revised properties response example using snake_case to the API spec.

Sequence Diagram

sequenceDiagram
  participant Client
  participant classify_intent as classify_intent node
  participant property_search as property_search node
  participant LLMClient
  participant SupabaseVectorClient

  Client->>classify_intent: state["message"]
  classify_intent->>LLMClient: classify(message)
  LLMClient-->>classify_intent: {intent, reasoning} or None
  classify_intent-->>Client: Intent enum (or FALLBACK)

  Client->>property_search: state["message"]
  property_search->>LLMClient: extract_property_criteria(message)
  LLMClient-->>property_search: criteria dict (or {})
  property_search->>SupabaseVectorClient: search_properties(criteria)
  SupabaseVectorClient-->>property_search: list[dict] properties
  property_search-->>Client: {properties, tool_results: {count, criteria}}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hop hop, keywords are gone today,
The LLM now leads the way!
RouteDecision guides each intent,
Supabase holds the data, well-meant.
GMS API joins the crew —
Text-to-SQL, shiny and new! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes align with issue #29 requirements, but some modifications (SpringClient dependency injection, legal document REST fallback) appear tangential to the stated property recommendation scope. Clarify whether SpringClient refactoring and legal document fallback are necessary for the core property recommendation feature or should be addressed separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing Text-to-SQL property recommendation and LLM-based intent classification.
Description check ✅ Passed The PR description follows the template structure with all required sections (변경 내용, 연결 이슈, 테스트) adequately filled, though some test checkboxes remain unchecked.
Linked Issues check ✅ Passed The PR implements the key FastAPI requirements from issue #29: LLM-based intent classification, PROPERTY_SEARCH routing, and search_properties tool via Text-to-SQL.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/29-chat-property-search

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend-ai/app/clients/llm_client.py`:
- Around line 88-89: The parsed JSON response from
extract_chat_completion_text() in the LLM client is not validated to ensure it's
a dictionary object before being returned. While the fallback value "{}" is
returned when extraction fails, valid JSON could still parse to a list, string,
or number, which would violate the dict[str, Any] contract expected by
extract_property_criteria(). After calling json.loads(text) in both locations
(around lines 88-89 and 110-111), validate that the parsed result is actually a
dictionary using isinstance() check, and return the fallback json.loads("{}")
only if the parsed value is confirmed to be a dict; otherwise return the
fallback value.

In `@backend-ai/app/clients/supabase_client.py`:
- Line 130: The unconditional `int(self.statement_timeout_ms)` cast in the
cursor.execute call for SET LOCAL statement_timeout can raise an exception if
statement_timeout_ms is unset or misconfigured, causing the query execution to
fail before it runs. Add validation or a default fallback before casting to
handle cases where statement_timeout_ms might be None or invalid, ensuring the
timeout is set to a safe default value when the property is not properly
configured.
- Around line 108-111: The loop iterating through range_map.items() appends
conditions and parameters without validating that the numeric criteria values
(max_* filters) are actually numeric. Before appending to conditions and params
in this loop, add validation or coercion logic to ensure criteria[key] is
numeric, and handle cases where the value cannot be converted to a number by
either skipping that condition gracefully or coercing it to an appropriate
default value instead of letting the invalid value fail during SQL execution.

In `@backend-ai/app/graph/nodes/property_search.py`:
- Around line 7-8: The LLMClient().extract_property_criteria method can return
non-object JSON (such as strings or arrays) which causes failures when passed to
SupabaseVectorClient().search_properties since it expects a dictionary with
mapping semantics. After calling extract_property_criteria on the state message,
normalize the returned criteria to ensure it is a dictionary before passing it
to the search_properties method. If the criteria is not already a dict, convert
it appropriately so that the SQL client receives the expected mapping structure.
- Around line 8-14: Wrap the SupabaseVectorClient().search_properties(criteria)
call in a try-except block to handle any errors gracefully. When an exception
occurs, catch it and return an empty list for properties instead of letting the
error propagate, while also adding error metadata to the tool_results dictionary
so the caller can understand that the search failed. This way the node will
degrade gracefully and continue graph execution instead of aborting.

In `@docs/08_API_SPEC.md`:
- Around line 394-402: The API documentation example in the properties object
section shows camelCase field names (buildingName, propertyType,
transactionType, monthlyRent, areaM2) and areaM2 as a string value, but the
actual API returns these fields in snake_case from the database without
camelCase transformation applied to the properties list. Update the
documentation example to use snake_case keys (building_name, property_type,
transaction_type, monthly_rent, area_m2) and ensure numeric fields like area_m2
are represented as numeric values (23.14) rather than strings ("23.14") to
accurately reflect what the API returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 355c2e1c-1e11-4f48-a885-28e0a04ff6aa

📥 Commits

Reviewing files that changed from the base of the PR and between 8374838 and f46d915.

📒 Files selected for processing (15)
  • AGENTS.md
  • CLAUDE.md
  • backend-ai/app/clients/llm_client.py
  • backend-ai/app/clients/spring_client.py
  • backend-ai/app/clients/supabase_client.py
  • backend-ai/app/core/config.py
  • backend-ai/app/graph/builder.py
  • backend-ai/app/graph/nodes/classify_intent.py
  • backend-ai/app/graph/nodes/property_search.py
  • backend-ai/app/graph/state.py
  • backend-ai/tests/test_agent_chat.py
  • backend-ai/tests/test_classify_intent.py
  • docs/02_ARCHITECTURE.md
  • docs/03_ADR.md
  • docs/08_API_SPEC.md

Comment thread backend-ai/app/clients/llm_client.py Outdated
Comment thread backend-ai/app/clients/supabase_client.py
Comment thread backend-ai/app/clients/supabase_client.py
Comment on lines +7 to +8
criteria = LLMClient().extract_property_criteria(state["message"])
properties = SupabaseVectorClient().search_properties(criteria)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Normalize criteria to a dict before DB search.

extract_property_criteria can return non-object JSON; passing that through causes downstream failures when the SQL client expects mapping semantics.

Proposed fix
 def property_search(state: AgentState) -> AgentState:
-    criteria = LLMClient().extract_property_criteria(state["message"])
+    raw_criteria = LLMClient().extract_property_criteria(state["message"])
+    criteria = raw_criteria if isinstance(raw_criteria, dict) else {}
     properties = SupabaseVectorClient().search_properties(criteria)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-ai/app/graph/nodes/property_search.py` around lines 7 - 8, The
LLMClient().extract_property_criteria method can return non-object JSON (such as
strings or arrays) which causes failures when passed to
SupabaseVectorClient().search_properties since it expects a dictionary with
mapping semantics. After calling extract_property_criteria on the state message,
normalize the returned criteria to ensure it is a dictionary before passing it
to the search_properties method. If the criteria is not already a dict, convert
it appropriately so that the SQL client receives the expected mapping structure.

Comment thread backend-ai/app/graph/nodes/property_search.py Outdated
Comment thread docs/08_API_SPEC.md Outdated
crolvlee and others added 2 commits June 23, 2026 17:15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@crolvlee
crolvlee merged commit 4f583b0 into develop Jun 23, 2026
1 check was pending
@HOKAGO-MEMORIES
HOKAGO-MEMORIES deleted the feat/29-chat-property-search branch June 25, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT][F-2] AI 에이전트 — 매물 추천

1 participant