tenai / app.py
aiqtech's picture
Create app.py
1729de6 verified
"""
TenAI PMAI Pro - ๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ AI ํ”Œ๋žซํผ
Zero Hardware | Data Monopoly | AI Transformation
Fireworks Vision API + Groq LLM
"""
import gradio as gr
import requests
import os, json, base64, re
from typing import Generator, Optional, List, Dict
import pandas as pd
import numpy as np
try:
import fitz
PDF_AVAILABLE = True
except ImportError:
PDF_AVAILABLE = False
try:
from PIL import Image
IMAGE_AVAILABLE = True
except ImportError:
IMAGE_AVAILABLE = False
try:
import folium
from folium.plugins import HeatMap
FOLIUM_AVAILABLE = True
except ImportError:
FOLIUM_AVAILABLE = False
try:
import plotly.express as px
import plotly.graph_objects as go
PLOTLY_AVAILABLE = True
except ImportError:
PLOTLY_AVAILABLE = False
try:
from datasets import load_dataset
DATASETS_AVAILABLE = True
except ImportError:
DATASETS_AVAILABLE = False
FIREWORKS_API_KEY = os.environ.get("FIREWORKS_API_KEY", "")
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
FIREWORKS_VISION_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
FIREWORKS_VISION_MODEL = "accounts/fireworks/models/qwen3-vl-235b-a22b-thinking"
DEMO_MODE = not FIREWORKS_API_KEY and not GROQ_API_KEY
groq_client = None
if GROQ_API_KEY:
try:
from groq import Groq
groq_client = Groq(api_key=GROQ_API_KEY)
except:
pass
PMAI_SYSTEM_PROMPT = """๋‹น์‹ ์€ TenAI์˜ PMAI(Property Management AI)์ž…๋‹ˆ๋‹ค. ๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ 24์‹œ๊ฐ„ AI ๋น„์„œ์ž…๋‹ˆ๋‹ค.
ํ•ต์‹ฌ ์—ญ๋Ÿ‰: Vision AI + RAG + ์ถ”๋ก ์—”์ง„ ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ๋ถ„์„, ๋น„์ •ํ˜• ๋ฌธ์„œ์™€ ๋„๋ฉด ์ดํ•ด
์ œ๊ณต ๊ฐ€์น˜: AMC(์ž์‚ฐ์šด์šฉ)-์‹ค์‹œ๊ฐ„ ๊ฐ€์น˜ํ‰๊ฐ€, PMC(์ž„๋Œ€๊ด€๋ฆฌ)-๊ณต์‹ค๊ด€๋ฆฌ ์ž๋™ํ™”, FMC(์‹œ์„ค๊ด€๋ฆฌ)-์‹œ์„ค์ ๊ฒ€ ์ž๋™ํ™”
ํŠน์ง•: Zero Hardware-์„ผ์„œ ์—†์ด ์ฆ‰์‹œ ๋„์ž…, ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ๋ถ„์„, ๋น„์šฉ ์ ˆ๊ฐ & ์ˆ˜์ต ์ฆ๋Œ€
ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ์ „๋ฌธ์ ์œผ๋กœ ์‘๋‹ตํ•˜์„ธ์š”."""
DOCUMENT_ANALYSIS_PROMPT = """๋‹น์‹ ์€ TenAI์˜ ๋ฌธ์„œ ๋ถ„์„ AI์ž…๋‹ˆ๋‹ค. ์—…๋กœ๋“œ๋œ ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ:
1. ํ•ต์‹ฌ ์ •๋ณด ์ถ”์ถœ 2. ์ž ์žฌ์  ๋ฆฌ์Šคํฌ ์‹๋ณ„ 3. ๋น„์šฉ ์ตœ์ ํ™” ํฌ์ธํŠธ ๋„์ถœ 4. ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ธ์‚ฌ์ดํŠธ ์ œ๊ณต"""
COST_ANALYSIS_PROMPT = """๋‹น์‹ ์€ TenAI์˜ ๋น„์šฉ ๋ถ„์„ AI์ž…๋‹ˆ๋‹ค. ์šด์˜๋น„์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜์—ฌ:
1. ๋น„์šฉ ๊ตฌ์กฐ ๋ถ„์„ 2. ๋ˆ„์ˆ˜ ์ง€์  ์‹๋ณ„ 3. ์ ˆ๊ฐ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ ๋„์ถœ 4. ROI ๊ธฐ๋ฐ˜ ์šฐ์„ ์ˆœ์œ„ ์ œ์•ˆ"""
SOMA_AGENTS = {
"coordinator": {"name": "๐ŸŽฏ ์ข…ํ•ฉ ์ฝ”๋””๋„ค์ดํ„ฐ", "role": "SOMA ํŒ€์žฅ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์ข…ํ•ฉ ์ฝ”๋””๋„ค์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ๊ฐ ์ „๋ฌธ๊ฐ€์˜ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ Executive Summary๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
"document_analyst": {"name": "๐Ÿ“‹ ๋ฌธ์„œ ๋ถ„์„๊ฐ€", "role": "๋ฌธ์„œ/๊ณ„์•ฝ์„œ ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ๋ฌธ์„œ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ, ์œ ์ง€๋ณด์ˆ˜ ๊ณ„์•ฝ ๋“ฑ์„ ์ •๋ฐ€ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
"financial_expert": {"name": "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€", "role": "๋น„์šฉ/์ˆ˜์ต ๋ถ„์„", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์žฌ๋ฌด ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์žฌ๋ฌด์  ์˜ํ–ฅ์„ ๋ถ„์„ํ•˜๊ณ  ๋น„์šฉ ์ ˆ๊ฐ ๋ฐฉ์•ˆ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
"legal_advisor": {"name": "โš–๏ธ ๋ฒ•๋ฅ  ์ž๋ฌธ๊ฐ€", "role": "๋ฒ•์  ๋ฆฌ์Šคํฌ ๊ฒ€ํ† ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ๋ฒ•๋ฅ  ์ž๋ฌธ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ๋ฒ•์  ๋ฆฌ์Šคํฌ, ๋ถˆ๋ฆฌํ•œ ์กฐํ•ญ์„ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
"facility_manager": {"name": "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž", "role": "์šด์˜/์‹œ์„ค ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์‹œ์„ค ๊ด€๋ฆฌ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์‹œ์„ค ์šด์˜ ๊ด€์ ์—์„œ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
"market_analyst": {"name": "๐Ÿ“Š ์ƒ๊ถŒ ๋ถ„์„๊ฐ€", "role": "์ž…์ง€/์ƒ๊ถŒ ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์ƒ๊ถŒ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ž…์ง€, ์ฃผ๋ณ€ ์ƒ๊ถŒ, ์œ ๋™์ธ๊ตฌ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."}
}
SEOUL_DISTRICTS = {
"๊ฐ•๋‚จ๊ตฌ": {"lat": 37.5172, "lng": 127.0473, "ํŠน์„ฑ": "IT/๊ธˆ์œต ์ค‘์‹ฌ, ๊ณ ๊ธ‰ ์˜คํ”ผ์Šค", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 85000},
"์„œ์ดˆ๊ตฌ": {"lat": 37.4837, "lng": 127.0324, "ํŠน์„ฑ": "๋ฒ•์กฐํƒ€์šด, ๊ต์œก/๋ฌธํ™”", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 75000},
"์†กํŒŒ๊ตฌ": {"lat": 37.5145, "lng": 127.1050, "ํŠน์„ฑ": "์ž ์‹ค ์ƒ๊ถŒ, ์ฃผ๊ฑฐ/์ƒ์—… ๋ณตํ•ฉ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 65000},
"๋งˆํฌ๊ตฌ": {"lat": 37.5663, "lng": 126.9014, "ํŠน์„ฑ": "ํ™๋Œ€/ํ•ฉ์ • ์ƒ๊ถŒ, ๋ฌธํ™”/์˜ˆ์ˆ ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 55000},
"์˜๋“ฑํฌ๊ตฌ": {"lat": 37.5264, "lng": 126.8963, "ํŠน์„ฑ": "์—ฌ์˜๋„ ๊ธˆ์œต, ํƒ€์ž„์Šคํ€˜์–ด", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 70000},
"์šฉ์‚ฐ๊ตฌ": {"lat": 37.5324, "lng": 126.9903, "ํŠน์„ฑ": "์ดํƒœ์›/ํ•œ๋‚จ, ์žฌ๊ฐœ๋ฐœ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 60000},
"์„ฑ๋™๊ตฌ": {"lat": 37.5634, "lng": 127.0369, "ํŠน์„ฑ": "์„ฑ์ˆ˜๋™ ํ•ซํ”Œ, ์ง€์‹์‚ฐ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 55000},
"์ข…๋กœ๊ตฌ": {"lat": 37.5735, "lng": 126.9790, "ํŠน์„ฑ": "๋„์‹ฌ CBD, ์ „ํ†ต ์ƒ๊ถŒ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 80000},
"์ค‘๊ตฌ": {"lat": 37.5641, "lng": 126.9979, "ํŠน์„ฑ": "๋ช…๋™/์„์ง€๋กœ, ๊ด€๊ด‘/์ƒ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 90000},
"๊ฐ•์„œ๊ตฌ": {"lat": 37.5510, "lng": 126.8495, "ํŠน์„ฑ": "๋งˆ๊ณก์ง€๊ตฌ, ์‚ฐ์—…๋‹จ์ง€", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 45000},
"๊ตฌ๋กœ๊ตฌ": {"lat": 37.4954, "lng": 126.8874, "ํŠน์„ฑ": "๋””์ง€ํ„ธ๋‹จ์ง€, ์‚ฐ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 40000},
"๊ธˆ์ฒœ๊ตฌ": {"lat": 37.4569, "lng": 126.8956, "ํŠน์„ฑ": "๊ฐ€์‚ฐ๋””์ง€ํ„ธ, IT", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 38000},
}
MARKET_REGIONS = {'์„œ์šธ': '์„œ์šธ_202506', '๊ฒฝ๊ธฐ': '๊ฒฝ๊ธฐ_202506', '๋ถ€์‚ฐ': '๋ถ€์‚ฐ_202506', '๋Œ€๊ตฌ': '๋Œ€๊ตฌ_202506', '์ธ์ฒœ': '์ธ์ฒœ_202506', '๊ด‘์ฃผ': '๊ด‘์ฃผ_202506', '๋Œ€์ „': '๋Œ€์ „_202506', '์šธ์‚ฐ': '์šธ์‚ฐ_202506', '์„ธ์ข…': '์„ธ์ข…_202506', '๊ฒฝ๋‚จ': '๊ฒฝ๋‚จ_202506', '๊ฒฝ๋ถ': '๊ฒฝ๋ถ_202506', '์ „๋‚จ': '์ „๋‚จ_202506', '์ „๋ถ': '์ „๋ถ_202506', '์ถฉ๋‚จ': '์ถฉ๋‚จ_202506', '์ถฉ๋ถ': '์ถฉ๋ถ_202506', '๊ฐ•์›': '๊ฐ•์›_202506', '์ œ์ฃผ': '์ œ์ฃผ_202506'}
class MarketDataLoader:
@staticmethod
def load_region_data(region: str, sample_size: int = 20000) -> pd.DataFrame:
if not DATASETS_AVAILABLE:
return pd.DataFrame()
try:
file_name = f"์†Œ์ƒ๊ณต์ธ์‹œ์žฅ์ง„ํฅ๊ณต๋‹จ_์ƒ๊ฐ€(์ƒ๊ถŒ)์ •๋ณด_{MARKET_REGIONS[region]}.csv"
dataset = load_dataset("ginipick/market", data_files=file_name, split="train")
df = dataset.to_pandas()
return df.sample(n=min(sample_size, len(df)), random_state=42)
except:
return pd.DataFrame()
@staticmethod
def load_multiple_regions(regions: List[str], sample_per_region: int = 20000) -> pd.DataFrame:
dfs = [MarketDataLoader.load_region_data(r, sample_per_region) for r in regions]
return pd.concat([d for d in dfs if not d.empty], ignore_index=True) if any(not d.empty for d in dfs) else pd.DataFrame()
class MarketAnalyzer:
def __init__(self, df: pd.DataFrame):
self.df = df
self.prepare_data()
def prepare_data(self):
for col in ['๊ฒฝ๋„', '์œ„๋„']:
if col in self.df.columns:
self.df[col] = pd.to_numeric(self.df[col], errors='coerce')
self.df = self.df.dropna(subset=['๊ฒฝ๋„', '์œ„๋„'])
if '์ธต์ •๋ณด' in self.df.columns:
self.df['์ธต์ •๋ณด_์ˆซ์ž'] = self.df['์ธต์ •๋ณด'].apply(self._parse_floor)
def _parse_floor(self, floor_str):
if pd.isna(floor_str): return None
s = str(floor_str)
if '์ง€ํ•˜' in s or 'B' in s:
m = re.search(r'\d+', s)
return -int(m.group()) if m else -1
m = re.search(r'\d+', s)
return int(m.group()) if m else None
def get_summary(self) -> Dict:
return {
'์ด์ ํฌ์ˆ˜': len(self.df),
'์—…์ข…์ˆ˜': self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].nunique() if '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' in self.df.columns else 0,
'์ƒ์œ„์—…์ข…': self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].value_counts().head(5).to_dict() if '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' in self.df.columns else {},
}
def create_category_chart(self):
if not PLOTLY_AVAILABLE or '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' not in self.df.columns: return None
top = self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].value_counts().head(15)
fig = px.bar(x=top.values, y=top.index, orientation='h', title='๐Ÿ† ์ƒ์œ„ ์—…์ข… TOP 15', color=top.values, color_continuous_scale='blues')
fig.update_layout(showlegend=False, height=450)
return fig
def create_district_chart(self):
if not PLOTLY_AVAILABLE or '์‹œ๊ตฐ๊ตฌ๋ช…' not in self.df.columns: return None
counts = self.df['์‹œ๊ตฐ๊ตฌ๋ช…'].value_counts().head(15)
fig = px.bar(x=counts.values, y=counts.index, orientation='h', title='๐Ÿ“ ์ง€์—ญ๋ณ„ ์ ํฌ ๋ฐ€์ง‘๋„', color=counts.values, color_continuous_scale='reds')
fig.update_layout(showlegend=False, height=450)
return fig
def create_heatmap(self, sample_size: int = 2000) -> str:
if not FOLIUM_AVAILABLE: return "<p>folium ์„ค์น˜ ํ•„์š”</p>"
df_sample = self.df.sample(n=min(sample_size, len(self.df)), random_state=42)
m = folium.Map(location=[df_sample['์œ„๋„'].mean(), df_sample['๊ฒฝ๋„'].mean()], zoom_start=11, tiles='cartodbpositron')
HeatMap([[r['์œ„๋„'], r['๊ฒฝ๋„']] for _, r in df_sample.iterrows()], radius=15, blur=25).add_to(m)
return m._repr_html_()
class AppState:
def __init__(self):
self.analyzer = None
app_state = AppState()
def encode_image_to_base64(image_path: str) -> str:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode('utf-8')
def get_image_mime_type(file_path: str) -> str:
ext = file_path.lower().split('.')[-1]
return {'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'webp': 'image/webp'}.get(ext, 'image/jpeg')
def extract_text_from_image_fireworks(image_path: str) -> str:
"""Fireworks AI Vision API๋ฅผ ์‚ฌ์šฉํ•œ ์ด๋ฏธ์ง€ OCR"""
if not FIREWORKS_API_KEY:
return """[๋ฐ๋ชจ ๋ชจ๋“œ] ์ด๋ฏธ์ง€ OCR ๊ฒฐ๊ณผ:
---
์ž„๋Œ€์ฐจ ๊ณ„์•ฝ์„œ
์ œ1์กฐ (๋ชฉ์ ) ์ž„๋Œ€์ธ์€ ์•„๋ž˜ ๋ถ€๋™์‚ฐ์„ ์ž„์ฐจ์ธ์—๊ฒŒ ์ž„๋Œ€ํ•œ๋‹ค.
์†Œ์žฌ์ง€: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123, 5์ธต
์ž„๋Œ€๋ฉด์ : 330.58ใŽก (100ํ‰)
์ž„๋Œ€๊ธฐ๊ฐ„: 2024.01.01 ~ 2026.12.31 (3๋…„)
๋ณด์ฆ๊ธˆ: ๊ธˆ ์‚ผ์–ต์›์ • (โ‚ฉ300,000,000)
์›”์ž„๋Œ€๋ฃŒ: ๊ธˆ ์ผ์ฒœ์˜ค๋ฐฑ๋งŒ์›์ • (โ‚ฉ15,000,000)
๊ด€๋ฆฌ๋น„: ํ‰๋‹น 25,000์› (์›” 250๋งŒ์›)
---
โš ๏ธ FIREWORKS_API_KEY ์„ค์ • ์‹œ ์‹ค์ œ OCR ๊ฒฐ๊ณผ ์ œ๊ณต"""
try:
base64_image = encode_image_to_base64(image_path)
mime_type = get_image_mime_type(image_path)
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {FIREWORKS_API_KEY}"
}
payload = {
"model": FIREWORKS_VISION_MODEL,
"max_tokens": 4096,
"temperature": 0.2,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": """์ด ์ด๋ฏธ์ง€๋Š” ๋ถ€๋™์‚ฐ ๊ด€๋ จ ๋ฌธ์„œ(๊ณ„์•ฝ์„œ, ๋„๋ฉด, ๊ด€๋ฆฌ๋ฌธ์„œ ๋“ฑ)์ž…๋‹ˆ๋‹ค.
์ด๋ฏธ์ง€์— ์žˆ๋Š” ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”.
ํ‘œ๋‚˜ ๋„ํ‘œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜์—ฌ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ์„ธ์š”.
ํ•œ๊ตญ์–ด์™€ ์˜์–ด ๋ชจ๋‘ ์ •ํ™•ํ•˜๊ฒŒ ์ธ์‹ํ•ด์ฃผ์„ธ์š”.
์ถ”์ถœํ•œ ํ…์ŠคํŠธ๋งŒ ์ถœ๋ ฅํ•˜๊ณ , ๋‹ค๋ฅธ ์„ค๋ช…์€ ํ•˜์ง€ ๋งˆ์„ธ์š”."""
},
{
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_image}"
}
}
]
}
]
}
response = requests.post(FIREWORKS_VISION_URL, headers=headers, json=payload, timeout=60)
if response.status_code == 200:
result = response.json()
content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
if content:
if "<think>" in content and "</think>" in content:
content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL).strip()
return content
return "โš ๏ธ OCR ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."
else:
return f"โŒ API ์˜ค๋ฅ˜ ({response.status_code}): {response.text[:200]}"
except requests.exceptions.Timeout:
return "โŒ API ์‘๋‹ต ์‹œ๊ฐ„ ์ดˆ๊ณผ. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."
except Exception as e:
return f"โŒ OCR ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {str(e)}"
def extract_text_from_pdf(file_path: str) -> str:
if not PDF_AVAILABLE:
return "โŒ PyMuPDF ์„ค์น˜ ํ•„์š”: pip install pymupdf"
try:
doc = fitz.open(file_path)
texts = [f"--- ํŽ˜์ด์ง€ {i+1} ---\n{page.get_text()}" for i, page in enumerate(doc) if page.get_text().strip()]
doc.close()
return "\n\n".join(texts) if texts else "โš ๏ธ ํ…์ŠคํŠธ ์ถ”์ถœ ์‹คํŒจ (์ด๋ฏธ์ง€ PDF์ผ ์ˆ˜ ์žˆ์Œ)"
except Exception as e:
return f"โŒ PDF ์˜ค๋ฅ˜: {e}"
def generate_response_fireworks(message: str, history: list, system_prompt: str) -> Generator:
"""Fireworks AI๋ฅผ ์‚ฌ์šฉํ•œ ํ…์ŠคํŠธ ์ƒ์„ฑ (์ŠคํŠธ๋ฆฌ๋ฐ)"""
if not FIREWORKS_API_KEY:
demo = f"""## ๐Ÿข PMAI ๋ถ„์„ ๊ฒฐ๊ณผ
**์งˆ๋ฌธ**: {message}
---
### ๐Ÿ“‹ ๋ถ„์„ ๋‚ด์šฉ
1. **ํ˜„ํ™ฉ**: ์ž…๋ ฅ ๋‚ด์šฉ ๊ธฐ๋ฐ˜ ์ž์‚ฐ๊ด€๋ฆฌ ๊ด€์  ๋ถ„์„ ์™„๋ฃŒ
2. **ํ•ต์‹ฌ**: Zero Hardware ์ ‘๊ทผ, ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ๋น„์šฉ ์ ˆ๊ฐ ํฌ์ธํŠธ ๋„์ถœ
3. **๊ถŒ์žฅ**: ์šด์˜๋น„์šฉ ๊ตฌ์กฐ ์žฌ๊ฒ€ํ† , ์—๋„ˆ์ง€ ํšจ์œจํ™”, ๊ณต์‹ค๋ฅ  ๊ด€๋ฆฌ ์ „๋žต
> โš ๏ธ ๋ฐ๋ชจ ๋ชจ๋“œ - FIREWORKS_API_KEY ์„ค์ • ์‹œ ์ƒ์„ธ ๋ถ„์„ ์ œ๊ณต"""
for i in range(0, len(demo), 20):
yield demo[:i+20]
return
messages = [{"role": "system", "content": system_prompt}]
for h in history:
if isinstance(h, (list, tuple)) and len(h) >= 2:
messages.extend([{"role": "user", "content": str(h[0])}, {"role": "assistant", "content": str(h[1])}])
messages.append({"role": "user", "content": message})
headers = {"Accept": "application/json", "Content-Type": "application/json", "Authorization": f"Bearer {FIREWORKS_API_KEY}"}
payload = {"model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507", "max_tokens": 4096, "temperature": 0.7, "stream": True, "messages": messages}
try:
response = requests.post(FIREWORKS_VISION_URL, headers=headers, json=payload, stream=True, timeout=60)
if response.status_code == 200:
full_response = ""
for line in response.iter_lines():
if line:
line_text = line.decode('utf-8')
if line_text.startswith('data: '):
data_str = line_text[6:]
if data_str.strip() == '[DONE]':
break
try:
data = json.loads(data_str)
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
full_response += content
clean = re.sub(r'<think>.*?</think>', '', full_response, flags=re.DOTALL).strip()
yield clean
except:
continue
else:
yield f"โŒ API ์˜ค๋ฅ˜: {response.status_code}"
except Exception as e:
yield f"โŒ ์˜ค๋ฅ˜: {e}"
def generate_response(message: str, history: list, system_prompt: str = PMAI_SYSTEM_PROMPT) -> Generator:
"""Groq ๋˜๋Š” Fireworks๋ฅผ ์‚ฌ์šฉํ•œ ์‘๋‹ต ์ƒ์„ฑ"""
if groq_client:
messages = [{"role": "system", "content": system_prompt}]
for h in history:
if isinstance(h, (list, tuple)) and len(h) >= 2:
messages.extend([{"role": "user", "content": str(h[0])}, {"role": "assistant", "content": str(h[1])}])
messages.append({"role": "user", "content": message})
try:
completion = groq_client.chat.completions.create(model="llama-3.3-70b-versatile", messages=messages, temperature=0.7, max_tokens=4096, stream=True)
response = ""
for chunk in completion:
if chunk.choices[0].delta.content:
response += chunk.choices[0].delta.content
yield response
return
except:
pass
yield from generate_response_fireworks(message, history, system_prompt)
def chat_respond(message: str, history: list):
if not message or not message.strip():
yield history or []
return
history = history or []
history_api = []
for h in history:
if isinstance(h, dict):
r, c = h.get("role", ""), h.get("content", "")
if r == "user": history_api.append([c, ""])
elif r == "assistant" and history_api: history_api[-1][1] = c
new_history = list(history) + [{"role": "user", "content": message}, {"role": "assistant", "content": ""}]
for chunk in generate_response(message, history_api, PMAI_SYSTEM_PROMPT):
new_history[-1] = {"role": "assistant", "content": chunk}
yield new_history
def run_soma_analysis(document_text: str, selected_agents: List[str]) -> Generator:
if not document_text.strip():
yield "๐Ÿ“„ ๋ฌธ์„œ ๋‚ด์šฉ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
return
if not selected_agents:
selected_agents = ["document_analyst", "financial_expert", "legal_advisor"]
output = "# ๐Ÿค– SOMA ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ํ˜‘์—… ๋ถ„์„\n\n---\n\n"
yield output
results = {}
for key in selected_agents:
agent = SOMA_AGENTS.get(key)
if not agent: continue
output += f"## {agent['name']}\n**์—ญํ• **: {agent['role']}\n\nโณ ๋ถ„์„ ์ค‘...\n\n"
yield output
prompt = f"{agent['prompt']}\n\n๋ถ„์„ํ•  ๋ฌธ์„œ:\n---\n{document_text[:6000]}\n---\n๊ตฌ์ฒด์ ์ธ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”."
agent_response = ""
for chunk in generate_response(prompt, [], agent['prompt']):
agent_response = chunk
results[key] = agent_response
output = output.replace("โณ ๋ถ„์„ ์ค‘...\n\n", f"{agent_response}\n\n---\n\n")
yield output
if len(results) > 1 and "coordinator" not in selected_agents:
output += f"## {SOMA_AGENTS['coordinator']['name']}\nโณ ์ข…ํ•ฉ ๋ถ„์„ ์ค‘...\n\n"
yield output
summary_prompt = f"๊ฐ ์ „๋ฌธ๊ฐ€ ๋ถ„์„์„ ์ข…ํ•ฉํ•˜์—ฌ Executive Summary๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”:\n" + "\n".join([f"### {SOMA_AGENTS[k]['name']}:\n{v[:1000]}" for k, v in results.items()])
coord_response = ""
for chunk in generate_response(summary_prompt, [], SOMA_AGENTS['coordinator']['prompt']):
coord_response = chunk
output = output.replace("โณ ์ข…ํ•ฉ ๋ถ„์„ ์ค‘...\n\n", f"{coord_response}\n\n")
yield output
yield output + "\nโœ… **SOMA ๋ถ„์„ ์™„๋ฃŒ**"
def analyze_document(document_text: str, document_type: str, file_upload: Optional[str] = None) -> Generator:
if file_upload:
ext = file_upload.lower().split('.')[-1]
if ext == 'pdf':
yield "๐Ÿ“„ PDF ํ…์ŠคํŠธ ์ถ”์ถœ ์ค‘..."
extracted = extract_text_from_pdf(file_upload)
if extracted.startswith("โŒ") or extracted.startswith("โš ๏ธ"):
yield extracted
return
document_text = extracted
yield f"๐Ÿ“„ PDF ์ถ”์ถœ ์™„๋ฃŒ ({len(extracted):,}์ž)\n\n๋ถ„์„ ์ค‘..."
elif ext in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
yield f"๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ OCR ์ฒ˜๋ฆฌ ์ค‘... (Fireworks Vision AI)"
extracted = extract_text_from_image_fireworks(file_upload)
if extracted.startswith("โŒ"):
yield extracted
return
document_text = extracted
yield f"๐Ÿ–ผ๏ธ OCR ์™„๋ฃŒ ({len(extracted):,}์ž)\n\n**์ถ”์ถœ๋œ ํ…์ŠคํŠธ:**\n```\n{extracted[:2000]}{'...' if len(extracted) > 2000 else ''}\n```\n\n๋ถ„์„ ์ค‘..."
else:
yield f"โŒ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: .{ext}"
return
if not document_text or not document_text.strip():
yield "๐Ÿ“„ ๋ฌธ์„œ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”."
return
prompt = f"""๋‹ค์Œ {document_type} ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:
---
{document_text[:8000]}
---
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„:
## ๐Ÿ“‹ ๋ฌธ์„œ ์š”์•ฝ (3์ค„)
## ๐Ÿ” ํ•ต์‹ฌ ์ •๋ณด
## โš ๏ธ ๋ฆฌ์Šคํฌ ํฌ์ธํŠธ
## ๐Ÿ’ก ์ตœ์ ํ™” ์ œ์•ˆ
## ๐Ÿ“Š ์•ก์…˜ ์•„์ดํ…œ"""
full_output = ""
for chunk in generate_response(prompt, [], DOCUMENT_ANALYSIS_PROMPT):
full_output = chunk
yield full_output
full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
yield full_output
mini_agents = [("legal_advisor", "โš–๏ธ ๋ฒ•๋ฅ  ์ž๋ฌธ๊ฐ€"), ("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€")]
for agent_key, agent_label in mini_agents:
agent = SOMA_AGENTS.get(agent_key)
if not agent: continue
full_output += f"### {agent_label}\n"
yield full_output + "๋ถ„์„ ์ค‘...\n"
mini_prompt = f"{agent['prompt']}\n\n๋ฌธ์„œ ๋‚ด์šฉ:\n{document_text[:3000]}\n\nํ•ต์‹ฌ ํฌ์ธํŠธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”. ๊ฐ ํฌ์ธํŠธ๋Š” 1-2๋ฌธ์žฅ์œผ๋กœ."
agent_response = ""
for chunk in generate_response(mini_prompt, [], agent['prompt']):
agent_response = chunk
full_output += f"{agent_response}\n\n"
yield full_output
def analyze_cost(building_name, monthly_rent, maintenance, utility, personnel, repair, other, vacancy_rate, additional_info) -> Generator:
total = maintenance + utility + personnel + repair + other
noi = monthly_rent * (1 - vacancy_rate/100) - total
cost_data = f"""๊ฑด๋ฌผ๋ช…: {building_name}
์›” ์ž„๋Œ€์ˆ˜์ž…: {monthly_rent:,.0f}์› | ๊ณต์‹ค๋ฅ : {vacancy_rate}%
์šด์˜๋น„์šฉ ๋‚ด์—ญ:
- ๊ด€๋ฆฌ๋น„: {maintenance:,.0f}์› ({maintenance/total*100:.1f}%)
- ์œ ํ‹ธ๋ฆฌํ‹ฐ: {utility:,.0f}์› ({utility/total*100:.1f}%)
- ์ธ๊ฑด๋น„: {personnel:,.0f}์› ({personnel/total*100:.1f}%)
- ์ˆ˜์„ ์œ ์ง€๋น„: {repair:,.0f}์› ({repair/total*100:.1f}%)
- ๊ธฐํƒ€: {other:,.0f}์› ({other/total*100:.1f}%)
- ์ด ์šด์˜๋น„์šฉ: {total:,.0f}์›
- ์ˆœ์šด์˜์ˆ˜์ต(NOI): {noi:,.0f}์›
์ถ”๊ฐ€์ •๋ณด: {additional_info or '์—†์Œ'}"""
prompt = f"""๊ฑด๋ฌผ ์šด์˜๋น„์šฉ์„ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:
{cost_data}
๋ถ„์„ ํ˜•์‹:
## ๐Ÿ“Š ๋น„์šฉ ๊ตฌ์กฐ ๋ถ„์„
## ๐Ÿ”ด ๋ˆ„์ˆ˜ ํฌ์ธํŠธ ์‹๋ณ„
## ๐Ÿ’ฐ ์ ˆ๊ฐ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ (๊ตฌ์ฒด์  ๊ธˆ์•ก ์ œ์‹œ)
## ๐Ÿ“ˆ ROI ๊ธฐ๋ฐ˜ ์šฐ์„ ์ˆœ์œ„
## ๐Ÿ’ต ์˜ˆ์ƒ ์ ˆ๊ฐ ํšจ๊ณผ (์›”๊ฐ„/์—ฐ๊ฐ„)"""
full_output = ""
for chunk in generate_response(prompt, [], COST_ANALYSIS_PROMPT):
full_output = chunk
yield full_output
full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
yield full_output
mini_agents = [("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€"), ("facility_manager", "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž")]
for agent_key, agent_label in mini_agents:
agent = SOMA_AGENTS.get(agent_key)
if not agent: continue
full_output += f"### {agent_label}\n"
yield full_output + "๋ถ„์„ ์ค‘...\n"
mini_prompt = f"{agent['prompt']}\n\n๋น„์šฉ ๋ฐ์ดํ„ฐ:\n{cost_data}\n\n๋‹น์‹ ์˜ ์ „๋ฌธ ๋ถ„์•ผ ๊ด€์ ์—์„œ ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ œ์‹œํ•ด์ฃผ์„ธ์š”."
agent_response = ""
for chunk in generate_response(mini_prompt, [], agent['prompt']):
agent_response = chunk
full_output += f"{agent_response}\n\n"
yield full_output
def create_seoul_map(selected: str = None) -> str:
if not FOLIUM_AVAILABLE:
return "<div style='padding:40px;text-align:center;'><p>folium ์„ค์น˜ ํ•„์š”</p></div>"
m = folium.Map(location=[37.5665, 126.9780], zoom_start=11, tiles='cartodbpositron')
for name, info in SEOUL_DISTRICTS.items():
color = 'red' if name == selected else 'blue'
popup = f"<b>{name}</b><br>{info['ํŠน์„ฑ']}<br>์ž„๋Œ€๋ฃŒ: {info['ํ‰๊ท ์ž„๋Œ€๋ฃŒ']:,}์›/ํ‰"
folium.Marker([info['lat'], info['lng']], popup=popup, tooltip=name, icon=folium.Icon(color=color, icon='building', prefix='fa')).add_to(m)
if name == selected:
folium.Circle([info['lat'], info['lng']], radius=1500, color='red', fill=True, fillOpacity=0.2).add_to(m)
return m._repr_html_()
def analyze_location(district: str) -> Generator:
if district not in SEOUL_DISTRICTS:
yield "์ง€์—ญ ์ •๋ณด ์—†์Œ"
return
info = SEOUL_DISTRICTS[district]
location_data = f"""์ง€์—ญ: ์„œ์šธ์‹œ {district}
ํŠน์„ฑ: {info['ํŠน์„ฑ']}
ํ‰๊ท  ์ž„๋Œ€๋ฃŒ: {info['ํ‰๊ท ์ž„๋Œ€๋ฃŒ']:,}์›/ํ‰
์œ„์น˜: ์œ„๋„ {info['lat']}, ๊ฒฝ๋„ {info['lng']}"""
prompt = f"""์„œ์šธ์‹œ {district}์˜ ์ƒ๊ถŒ ๋ฐ ๋ถ€๋™์‚ฐ ์ž…์ง€๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
{location_data}
๋‹ค์Œ ๊ด€์ ์—์„œ ์ƒ์„ธํžˆ ๋ถ„์„:
## ๐Ÿ“ ์ƒ๊ถŒ ํŠน์„ฑ ๋ถ„์„
(์ฃผ์š” ์—…์ข…, ์œ ๋™์ธ๊ตฌ ํŒจํ„ด, ์†Œ๋น„ ํŠน์„ฑ)
## ๐Ÿข ์˜คํ”ผ์Šค/์ƒ๊ฐ€ ์‹œ์žฅ ํ˜„ํ™ฉ
(์ž„๋Œ€๋ฃŒ ์ˆ˜์ค€, ๊ณต์‹ค๋ฅ  ์ถ”์ด, ์ˆ˜์š”-๊ณต๊ธ‰)
## ๐Ÿ“ˆ ํˆฌ์ž ๋งค๋ ฅ๋„ ํ‰๊ฐ€
(์ž์‚ฐ๊ฐ€์น˜ ์ƒ์Šน ๊ฐ€๋Šฅ์„ฑ, ๊ฐœ๋ฐœ ํ˜ธ์žฌ)
## โš ๏ธ ๋ฆฌ์Šคํฌ ์š”์ธ
## ๐ŸŽฏ ์ถ”์ฒœ ์ „๋žต"""
full_output = ""
for chunk in generate_response(prompt, [], SOMA_AGENTS['market_analyst']['prompt']):
full_output = chunk
yield full_output
full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
yield full_output
mini_agents = [("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€"), ("facility_manager", "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž")]
for agent_key, agent_label in mini_agents:
agent = SOMA_AGENTS.get(agent_key)
if not agent: continue
full_output += f"### {agent_label}\n"
yield full_output + "๋ถ„์„ ์ค‘...\n"
mini_prompt = f"{agent['prompt']}\n\n์ž…์ง€ ์ •๋ณด:\n{location_data}\n\n์ด ์ง€์—ญ์—์„œ ์ž์‚ฐ๊ด€๋ฆฌ ์‹œ ๋‹น์‹ ์˜ ์ „๋ฌธ ๋ถ„์•ผ ๊ด€์ ์—์„œ ํ•ต์‹ฌ ์กฐ์–ธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ œ์‹œํ•ด์ฃผ์„ธ์š”."
agent_response = ""
for chunk in generate_response(mini_prompt, [], agent['prompt']):
agent_response = chunk
full_output += f"{agent_response}\n\n"
yield full_output
def create_cost_chart(m, u, p, r, o):
if not PLOTLY_AVAILABLE: return None
fig = go.Figure(data=[go.Pie(labels=['๊ด€๋ฆฌ๋น„','์œ ํ‹ธ๋ฆฌํ‹ฐ','์ธ๊ฑด๋น„','์ˆ˜์„ ๋น„','๊ธฐํƒ€'], values=[m,u,p,r,o], hole=0.4, marker_colors=['#3B82F6','#10B981','#F59E0B','#EF4444','#8B5CF6'])])
fig.update_layout(title='์›”๊ฐ„ ์šด์˜๋น„์šฉ', height=350, paper_bgcolor='#ffffff', font=dict(color='#1e293b'))
return fig
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap');
.gradio-container { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #f1f5f9 100%) !important; font-family: 'Noto Sans KR', sans-serif !important; min-height: 100vh; }
.gr-button-primary { background: linear-gradient(135deg, #2563eb, #3b82f6) !important; border: none !important; color: white !important; font-weight: 600 !important; box-shadow: 0 4px 14px rgba(37,99,235,0.35) !important; }
.gr-button-primary:hover { background: linear-gradient(135deg, #1d4ed8, #2563eb) !important; transform: translateY(-1px) !important; box-shadow: 0 6px 20px rgba(37,99,235,0.4) !important; }
.gr-panel, .block { background: #ffffff !important; border: 1px solid #e2e8f0 !important; border-radius: 16px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; }
textarea, input[type="text"], input[type="number"] { background: #ffffff !important; border: 2px solid #e2e8f0 !important; color: #1e293b !important; border-radius: 10px !important; }
textarea:focus, input:focus { border-color: #3b82f6 !important; box-shadow: 0 0 0 3px rgba(59,130,246,0.15) !important; }
label, .gr-input-label { color: #334155 !important; font-weight: 500 !important; }
.gr-markdown { color: #334155 !important; }
.gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { color: #1e40af !important; font-weight: 700 !important; }
.gr-markdown h1 { font-size: 1.8em !important; }
.gr-markdown h2 { font-size: 1.4em !important; }
.gr-markdown h3 { font-size: 1.2em !important; }
.gr-chatbot { background: #ffffff !important; border: 1px solid #e2e8f0 !important; border-radius: 16px !important; }
.gr-tab-nav { background: #f1f5f9 !important; border-radius: 12px !important; padding: 4px !important; }
.gr-tab-nav button { color: #64748b !important; font-weight: 500 !important; border-radius: 8px !important; }
.gr-tab-nav button.selected { background: #ffffff !important; color: #2563eb !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; }
.gr-accordion { background: #f8fafc !important; border: 1px solid #e2e8f0 !important; border-radius: 12px !important; }
.gr-dropdown { background: #ffffff !important; border: 2px solid #e2e8f0 !important; border-radius: 10px !important; }
.gr-checkbox-group { background: #f8fafc !important; border-radius: 10px !important; padding: 12px !important; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #64748b; }
footer { display: none !important; }
"""
def create_demo():
with gr.Blocks(title="TenAI PMAI Pro", css=CSS) as demo:
gr.HTML("""<div style="text-align:center;padding:35px 20px;background:linear-gradient(135deg,#ffffff,#f8fafc);border-radius:20px;border:1px solid #e2e8f0;margin-bottom:25px;box-shadow:0 8px 30px rgba(0,0,0,0.08);">
<h1 style="color:#1e40af;font-size:2.6em;margin:0;font-weight:800;">๐Ÿข TenAI PMAI Pro</h1>
<p style="color:#475569;margin:12px 0 5px 0;font-size:1.15em;font-weight:500;">๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ AI ํ”Œ๋žซํผ</p>
<p style="color:#64748b;margin:5px 0 20px 0;font-size:0.95em;">"ํ•˜๋“œ์›จ์–ด ์—†๋Š” ๊ฑด๋ฌผ ์šด์˜์ฒด์ œ(OS), TenAI"</p>
<div style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;">
<span style="background:linear-gradient(135deg,#2563eb,#3b82f6);padding:10px 22px;border-radius:25px;color:white;font-size:0.9em;font-weight:600;box-shadow:0 4px 12px rgba(37,99,235,0.3);">โšก Zero Hardware</span>
<span style="background:linear-gradient(135deg,#059669,#10b981);padding:10px 22px;border-radius:25px;color:white;font-size:0.9em;font-weight:600;box-shadow:0 4px 12px rgba(16,185,129,0.3);">๐Ÿ–ผ๏ธ Fireworks Vision AI</span>
<span style="background:linear-gradient(135deg,#7c3aed,#8b5cf6);padding:10px 22px;border-radius:25px;color:white;font-size:0.9em;font-weight:600;box-shadow:0 4px 12px rgba(139,92,246,0.3);">๐Ÿค– SOMA Multi-Agent</span>
</div></div>""")
with gr.Tabs():
with gr.Tab("๐Ÿ’ฌ PMAI ์ƒ๋‹ด"):
gr.Markdown("### ๐Ÿค– 24์‹œ๊ฐ„ AI ์ž์‚ฐ๊ด€๋ฆฌ ๋น„์„œ")
chatbot = gr.Chatbot(height=400)
with gr.Row():
msg = gr.Textbox(placeholder="์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”...", show_label=False, scale=9)
btn = gr.Button("์ „์†ก", variant="primary", scale=1)
gr.Examples(["๊ณต์‹ค๋ฅ ์„ ๋‚ฎ์ถ”๋Š” ๋ฐฉ๋ฒ•์€?", "๊ฑด๋ฌผ ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ ์ ˆ๊ฐ ์ „๋žต", "์ž„๋Œ€์ฐจ ๊ณ„์•ฝ ๊ฐฑ์‹  ์‹œ ์ฃผ์˜์ "], inputs=msg)
msg.submit(chat_respond, [msg, chatbot], [chatbot]).then(lambda: "", None, [msg])
btn.click(chat_respond, [msg, chatbot], [chatbot]).then(lambda: "", None, [msg])
with gr.Tab("๐Ÿ“„ ๋ฌธ์„œ ๋ถ„์„"):
gr.Markdown("### ๐Ÿ“‹ Vision AI ๋ฌธ์„œ ๋ถ„์„\n**PDF** ๋˜๋Š” **์ด๋ฏธ์ง€**(๊ณ„์•ฝ์„œ ์‚ฌ์ง„, ์Šค์บ”๋ณธ) ์—…๋กœ๋“œ ์‹œ ์ž๋™ OCR ์ฒ˜๋ฆฌ")
with gr.Row():
with gr.Column(scale=1):
doc_type = gr.Dropdown(["์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ", "์œ ์ง€๋ณด์ˆ˜ ๋ฌธ์„œ", "์‹œ์„ค์ ๊ฒ€ ๋ณด๊ณ ์„œ", "๊ด€๋ฆฌ๋น„ ๋‚ด์—ญ์„œ", "๊ฑด๋ฌผ ๋„๋ฉด", "๊ธฐํƒ€"], value="์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ", label="๋ฌธ์„œ ์œ ํ˜•")
file_upload = gr.File(label="๐Ÿ“Ž ํŒŒ์ผ ์—…๋กœ๋“œ (PDF/์ด๋ฏธ์ง€)", file_types=[".pdf",".jpg",".jpeg",".png",".gif",".webp"], type="filepath")
gr.Markdown("<small>โœ… ์ง€์›: PDF, JPG, PNG, GIF, WEBP | ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€๋Š” Fireworks Vision AI๋กœ OCR</small>")
doc_input = gr.Textbox(lines=6, placeholder="๋˜๋Š” ํ…์ŠคํŠธ ์ง์ ‘ ์ž…๋ ฅ...", label="๋ฌธ์„œ ๋‚ด์šฉ")
analyze_btn = gr.Button("๐Ÿ” ๋ถ„์„ ์‹œ์ž‘", variant="primary", size="lg")
with gr.Column(scale=1):
analysis_output = gr.Markdown("### ๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ\nํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ ํ›„ ๋ถ„์„ ์‹œ์ž‘")
analyze_btn.click(analyze_document, [doc_input, doc_type, file_upload], analysis_output)
with gr.Tab("๐Ÿ’ฐ ๋น„์šฉ ๋ถ„์„"):
gr.Markdown("### ๐Ÿ“ˆ ์šด์˜๋น„์šฉ ์ตœ์ ํ™” ๋ถ„์„")
with gr.Row():
with gr.Column():
building = gr.Textbox(label="๊ฑด๋ฌผ๋ช…", value="๊ฐ•๋‚จํ…Œํฌํƒ€์›Œ")
rent = gr.Number(label="์›” ์ž„๋Œ€์ˆ˜์ž… (์›)", value=50000000)
vacancy = gr.Slider(0, 100, 10, label="๊ณต์‹ค๋ฅ  (%)")
gr.Markdown("#### ์›”๊ฐ„ ์šด์˜๋น„์šฉ")
m_cost = gr.Number(label="๊ด€๋ฆฌ๋น„", value=5000000)
u_cost = gr.Number(label="์œ ํ‹ธ๋ฆฌํ‹ฐ", value=8000000)
p_cost = gr.Number(label="์ธ๊ฑด๋น„", value=12000000)
r_cost = gr.Number(label="์ˆ˜์„ ์œ ์ง€๋น„", value=3000000)
o_cost = gr.Number(label="๊ธฐํƒ€", value=2000000)
add_info = gr.Textbox(label="์ถ”๊ฐ€ ์ •๋ณด", lines=2)
cost_btn = gr.Button("๐Ÿ’ก ๋น„์šฉ ๋ถ„์„", variant="primary")
with gr.Column():
cost_chart = gr.Plot()
cost_output = gr.Markdown("### ๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ")
cost_btn.click(lambda m,u,p,r,o: create_cost_chart(m,u,p,r,o), [m_cost,u_cost,p_cost,r_cost,o_cost], cost_chart)
cost_btn.click(analyze_cost, [building,rent,m_cost,u_cost,p_cost,r_cost,o_cost,vacancy,add_info], cost_output)
with gr.Tab("๐Ÿ—บ๏ธ ์ž…์ง€ ๋ถ„์„"):
gr.Markdown("### ๐Ÿ“ ์„œ์šธ์‹œ ์ƒ๊ถŒ ๋ถ„์„")
with gr.Row():
with gr.Column():
district = gr.Dropdown(list(SEOUL_DISTRICTS.keys()), value="๊ฐ•๋‚จ๊ตฌ", label="์ง€์—ญ ์„ ํƒ")
loc_btn = gr.Button("๐Ÿ“Š ์ž…์ง€ ๋ถ„์„", variant="primary")
with gr.Column():
map_html = gr.HTML(value=create_seoul_map())
loc_output = gr.Markdown("### ๋ถ„์„ ๊ฒฐ๊ณผ")
district.change(create_seoul_map, [district], [map_html])
loc_btn.click(analyze_location, [district], loc_output)
with gr.Tab("๐Ÿค– SOMA ํ˜‘์—…"):
gr.Markdown("### ๐Ÿค– ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ํ˜‘์—… ๋ถ„์„\n6๋ช…์˜ AI ์ „๋ฌธ๊ฐ€๊ฐ€ ๋ฌธ์„œ๋ฅผ ๋‹ค๊ฐ๋„๋กœ ๋ถ„์„")
with gr.Row():
with gr.Column():
soma_agents = gr.CheckboxGroup([("๐Ÿ“‹ ๋ฌธ์„œ๋ถ„์„๊ฐ€","document_analyst"),("๐Ÿ’ฐ ์žฌ๋ฌด์ „๋ฌธ๊ฐ€","financial_expert"),("โš–๏ธ ๋ฒ•๋ฅ ์ž๋ฌธ๊ฐ€","legal_advisor"),("๐Ÿ”ง ์‹œ์„ค๊ด€๋ฆฌ์ž","facility_manager"),("๐Ÿ“Š ์ƒ๊ถŒ๋ถ„์„๊ฐ€","market_analyst")], value=["document_analyst","financial_expert","legal_advisor"], label="๋ถ„์„ ํŒ€")
soma_file = gr.File(label="ํŒŒ์ผ ์—…๋กœ๋“œ", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath")
soma_text = gr.Textbox(lines=5, placeholder="๋˜๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ...", label="๋ฌธ์„œ ๋‚ด์šฉ")
soma_btn = gr.Button("๐Ÿš€ SOMA ๋ถ„์„", variant="primary")
with gr.Column():
soma_output = gr.Markdown("### SOMA ๋ถ„์„ ๊ฒฐ๊ณผ")
def soma_with_file(text, agents, file):
doc = text or ""
if file:
ext = file.lower().split('.')[-1]
if ext == 'pdf':
doc = extract_text_from_pdf(file)
elif ext in ['jpg','jpeg','png','gif','webp']:
doc = extract_text_from_image_fireworks(file)
if doc.startswith("โŒ"):
yield doc
return
if not doc.strip():
yield "๋ฌธ์„œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
return
yield from run_soma_analysis(doc, agents)
soma_btn.click(soma_with_file, [soma_text, soma_agents, soma_file], soma_output)
with gr.Tab("โ„น๏ธ About"):
gr.Markdown("""
## ๐Ÿข TenAI PMAI Pro
### ๋น„์ „: "ํ•˜๋“œ์›จ์–ด ์—†๋Š” ๊ฑด๋ฌผ ์šด์˜์ฒด์ œ(OS)"
### ํ•ต์‹ฌ ๊ธฐ๋Šฅ
| ๊ธฐ๋Šฅ | ์„ค๋ช… |
|-----|-----|
| ๐Ÿ–ผ๏ธ **Fireworks Vision AI** | ์ด๋ฏธ์ง€ OCR (Qwen3-VL-235B) |
| ๐Ÿ“„ **๋ฌธ์„œ ๋ถ„์„** | PDF/์ด๋ฏธ์ง€ ์ž๋™ ํ…์ŠคํŠธ ์ถ”์ถœ ๋ฐ ๋ถ„์„ |
| ๐Ÿค– **SOMA ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ** | 6๋ช… AI ์ „๋ฌธ๊ฐ€ ํ˜‘์—… |
| ๐Ÿ—บ๏ธ **์ƒ๊ถŒ ๋ถ„์„** | ์„œ์šธ์‹œ 12๊ฐœ ๊ตฌ ์ž…์ง€ ๋ถ„์„ |
### API ์„ค์ •
```
FIREWORKS_API_KEY=your_key # Vision AI + LLM
GROQ_API_KEY=your_key # ๋น ๋ฅธ LLM (์„ ํƒ)
```
### Contact
๐Ÿ“ง ten@tenspace.co.kr | ๐Ÿ“ฑ 010-2710-6246
""")
gr.HTML("<div style='text-align:center;padding:20px;margin-top:10px;border-top:1px solid #e2e8f0;background:#f8fafc;border-radius:0 0 16px 16px;'><p style='color:#64748b;font-size:0.9em;margin:0;'>๐Ÿš€ Powered by <strong style='color:#2563eb;'>Ten-AX Engine</strong> | Fireworks Vision AI | SOMA Multi-Agent</p></div>")
return demo
if __name__ == "__main__":
demo = create_demo()
demo.launch(server_name="0.0.0.0", server_port=7860)