Suwon_multiculturalism / agents /main_agent.py
helloguddn (PC helloguddn)
fix: 모델 수정
9b49a5b
Raw History Blame Contribute Delete
7.35 kB
"""Single-agent orchestrator using Gemini function calling."""
from __future__ import annotations
import asyncio
import google.generativeai as genai
from schemas.chat import ChatRequest, ChatResponse, MapPin, WarningCard
from tools import contract_scanner, dlp_tool, labor_law_rag, maps_tool, public_data_tool
from services import gemini_service, storage_service
_LANG_NAMES = {"ko": "한국어", "vi": "베트남어", "en": "English"}
_DISCLAIMERS = {
"ko": "⚠️ 본 서비스는 법적 조언이 아닌 참고용 가이드입니다. 최종 판단은 전문가와 상담하세요.",
"vi": "⚠️ Dịch vụ này chỉ mang tính tham khảo, không phải tư vấn pháp lý. Hãy tham khảo ý kiến chuyên gia.",
"en": "⚠️ This service is for reference only and not legal advice. Please consult a professional.",
}
_SYSTEM_PROMPT = """당신은 Co-Local입니다. 수원시에 거주하는 이주 근로자를 위한 노동권 전문 AI 어시스턴트입니다.
주요 역할:
- 근로계약서의 법 위반 조항 분석 및 설명
- 한국 노동법(임금·근로시간·해고·연차·4대보험 등) 질문 답변
- 주변 노동청·복지센터·법률상담 기관 안내
- 임금체불 등 노동권 침해 상황 대처 방법 안내
반드시 {LANGUAGE}로 응답하세요.
답변은 명확하고 실용적이어야 하며, 구체적인 연락처와 다음 행동 지침을 포함하세요.
법적 책임 면책 문구를 항상 답변 말미에 포함하세요."""
async def run_agent(request: ChatRequest) -> ChatResponse:
gemini_service.configure()
language = request.language
session_id = request.session_id
# 1. Contract image pre-processing (before main agent loop)
warning_cards: list[WarningCard] = []
contract_context = ""
if request.image_base64:
scan_result = await contract_scanner.scan_contract_image(
request.image_base64, language
)
for v in scan_result.get("violations", []):
warning_cards.append(
WarningCard(
title=v.get("title", "위반 항목"),
content=v.get("content", ""),
severity=v.get("severity", "medium"),
)
)
summary = scan_result.get("summary", "")
if summary:
contract_context = f"\n\n[계약서 분석 완료]\n{summary}"
if warning_cards:
issues = "\n".join(f"- {w.title}: {w.content}" for w in warning_cards)
contract_context += f"\n\n발견된 문제:\n{issues}"
# 2. Per-turn mutable state (closures below write into these)
map_pins_buffer: list[dict] = []
# 3. Tool functions — closures that capture map_pins_buffer
def search_labor_law(query: str) -> str:
"""한국 노동법 관련 법조항을 검색합니다. 임금·근로시간·해고·연차·4대보험·임금체불 등 노동 관련 질문에 사용하세요.
Args:
query: 검색할 키워드 또는 질문 (예: '최저임금', '연장근무 수당', '부당해고 신고')
"""
return labor_law_rag.search_labor_law(query)
def find_nearby_centers(latitude: float, longitude: float, center_type: str = "all") -> str:
"""사용자 위치 근처의 노동청·복지센터·법률상담 기관을 찾아 지도에 핀을 꽂습니다.
Args:
latitude: 위도 (사용자 GPS 또는 메시지에서 추출)
longitude: 경도 (사용자 GPS 또는 메시지에서 추출)
center_type: 'labor'(노동청/고용센터), 'welfare'(복지센터/다문화센터), 'legal'(법률상담), 'all'(전체)
"""
desc, pins = maps_tool.find_nearby_centers(latitude, longitude, center_type)
map_pins_buffer.extend(pins)
return desc
def get_center_info(center_type: str = "all") -> str:
"""수원시 외국인 지원 기관의 전체 목록과 연락처를 조회합니다.
Args:
center_type: 'labor', 'welfare', 'legal', 'all'
"""
return public_data_tool.get_center_info(center_type)
# 4. Build model with tools
lang = _LANG_NAMES.get(language, "한국어")
system = _SYSTEM_PROMPT.replace("{LANGUAGE}", lang)
model = genai.GenerativeModel(
model_name="gemini-3.1-pro",
tools=[search_labor_law, find_nearby_centers, get_center_info],
system_instruction=system,
)
history = storage_service.get_history(session_id)
chat = model.start_chat(history=history)
# 5. Build message
user_text = dlp_tool.mask_pii(request.message)
if request.location:
user_text += f"\n[현재 위치: 위도 {request.location.lat}, 경도 {request.location.lng}]"
user_text += contract_context
msg_parts: list = []
if request.image_base64:
import base64, io
from PIL import Image
img_bytes = base64.b64decode(request.image_base64)
img = Image.open(io.BytesIO(img_bytes))
msg_parts.append(img)
msg_parts.append(user_text)
# 6. Agent loop — manual function calling for full control
response = await asyncio.to_thread(chat.send_message, msg_parts)
for _ in range(8):
fn_calls = [
p.function_call
for p in response.parts
if hasattr(p, "function_call") and p.function_call.name
]
if not fn_calls:
break
tool_map = {
"search_labor_law": search_labor_law,
"find_nearby_centers": find_nearby_centers,
"get_center_info": get_center_info,
}
tool_responses = []
for fc in fn_calls:
fn = tool_map.get(fc.name)
try:
result = fn(**dict(fc.args)) if fn else f"알 수 없는 도구: {fc.name}"
except Exception as exc:
result = f"도구 오류: {exc}"
tool_responses.append(
genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name=fc.name,
response={"result": result},
)
)
)
response = await asyncio.to_thread(chat.send_message, tool_responses)
# 7. Collect final reply text
reply_parts = [p.text for p in response.parts if hasattr(p, "text") and p.text]
reply = dlp_tool.mask_pii("".join(reply_parts))
# Append disclaimer if labor / contract content was involved
if warning_cards or map_pins_buffer:
reply += f"\n\n{_DISCLAIMERS.get(language, _DISCLAIMERS['ko'])}"
# 8. Persist session history
storage_service.save_history(session_id, list(chat.history))
# 9. Build next actions
next_actions: list[str] = []
if warning_cards:
next_actions += [
"고용노동부 신고: ☎1350 (무료, 외국어 지원)",
"수원시 마을변호사 무료상담: ☎1899-3300",
]
if map_pins_buffer:
next_actions.append("지도에서 가까운 기관 위치 확인")
return ChatResponse(
session_id=session_id,
reply=reply,
warning_cards=warning_cards,
map_pins=[MapPin(**p) for p in map_pins_buffer],
next_actions=next_actions,
)