aiqtech commited on
Commit
1729de6
ยท
verified ยท
1 Parent(s): 3b0cb17

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +623 -0
app.py ADDED
@@ -0,0 +1,623 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TenAI PMAI Pro - ๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ AI ํ”Œ๋žซํผ
3
+ Zero Hardware | Data Monopoly | AI Transformation
4
+ Fireworks Vision API + Groq LLM
5
+ """
6
+ import gradio as gr
7
+ import requests
8
+ import os, json, base64, re
9
+ from typing import Generator, Optional, List, Dict
10
+ import pandas as pd
11
+ import numpy as np
12
+ try:
13
+ import fitz
14
+ PDF_AVAILABLE = True
15
+ except ImportError:
16
+ PDF_AVAILABLE = False
17
+ try:
18
+ from PIL import Image
19
+ IMAGE_AVAILABLE = True
20
+ except ImportError:
21
+ IMAGE_AVAILABLE = False
22
+ try:
23
+ import folium
24
+ from folium.plugins import HeatMap
25
+ FOLIUM_AVAILABLE = True
26
+ except ImportError:
27
+ FOLIUM_AVAILABLE = False
28
+ try:
29
+ import plotly.express as px
30
+ import plotly.graph_objects as go
31
+ PLOTLY_AVAILABLE = True
32
+ except ImportError:
33
+ PLOTLY_AVAILABLE = False
34
+ try:
35
+ from datasets import load_dataset
36
+ DATASETS_AVAILABLE = True
37
+ except ImportError:
38
+ DATASETS_AVAILABLE = False
39
+ FIREWORKS_API_KEY = os.environ.get("FIREWORKS_API_KEY", "")
40
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
41
+ FIREWORKS_VISION_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
42
+ FIREWORKS_VISION_MODEL = "accounts/fireworks/models/qwen3-vl-235b-a22b-thinking"
43
+ DEMO_MODE = not FIREWORKS_API_KEY and not GROQ_API_KEY
44
+ groq_client = None
45
+ if GROQ_API_KEY:
46
+ try:
47
+ from groq import Groq
48
+ groq_client = Groq(api_key=GROQ_API_KEY)
49
+ except:
50
+ pass
51
+ PMAI_SYSTEM_PROMPT = """๋‹น์‹ ์€ TenAI์˜ PMAI(Property Management AI)์ž…๋‹ˆ๋‹ค. ๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ 24์‹œ๊ฐ„ AI ๋น„์„œ์ž…๋‹ˆ๋‹ค.
52
+ ํ•ต์‹ฌ ์—ญ๋Ÿ‰: Vision AI + RAG + ์ถ”๋ก ์—”์ง„ ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ๋ถ„์„, ๋น„์ •ํ˜• ๋ฌธ์„œ์™€ ๋„๋ฉด ์ดํ•ด
53
+ ์ œ๊ณต ๊ฐ€์น˜: AMC(์ž์‚ฐ์šด์šฉ)-์‹ค์‹œ๊ฐ„ ๊ฐ€์น˜ํ‰๊ฐ€, PMC(์ž„๋Œ€๊ด€๋ฆฌ)-๊ณต์‹ค๊ด€๋ฆฌ ์ž๋™ํ™”, FMC(์‹œ์„ค๊ด€๋ฆฌ)-์‹œ์„ค์ ๊ฒ€ ์ž๋™ํ™”
54
+ ํŠน์ง•: Zero Hardware-์„ผ์„œ ์—†์ด ์ฆ‰์‹œ ๋„์ž…, ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ๋ถ„์„, ๋น„์šฉ ์ ˆ๊ฐ & ์ˆ˜์ต ์ฆ๋Œ€
55
+ ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ์ „๋ฌธ์ ์œผ๋กœ ์‘๋‹ตํ•˜์„ธ์š”."""
56
+ DOCUMENT_ANALYSIS_PROMPT = """๋‹น์‹ ์€ TenAI์˜ ๋ฌธ์„œ ๋ถ„์„ AI์ž…๋‹ˆ๋‹ค. ์—…๋กœ๋“œ๋œ ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ:
57
+ 1. ํ•ต์‹ฌ ์ •๋ณด ์ถ”์ถœ 2. ์ž ์žฌ์  ๋ฆฌ์Šคํฌ ์‹๋ณ„ 3. ๋น„์šฉ ์ตœ์ ํ™” ํฌ์ธํŠธ ๋„์ถœ 4. ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ธ์‚ฌ์ดํŠธ ์ œ๊ณต"""
58
+ COST_ANALYSIS_PROMPT = """๋‹น์‹ ์€ TenAI์˜ ๋น„์šฉ ๋ถ„์„ AI์ž…๋‹ˆ๋‹ค. ์šด์˜๋น„์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜์—ฌ:
59
+ 1. ๋น„์šฉ ๊ตฌ์กฐ ๋ถ„์„ 2. ๋ˆ„์ˆ˜ ์ง€์  ์‹๋ณ„ 3. ์ ˆ๊ฐ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ ๋„์ถœ 4. ROI ๊ธฐ๋ฐ˜ ์šฐ์„ ์ˆœ์œ„ ์ œ์•ˆ"""
60
+ SOMA_AGENTS = {
61
+ "coordinator": {"name": "๐ŸŽฏ ์ข…ํ•ฉ ์ฝ”๋””๋„ค์ดํ„ฐ", "role": "SOMA ํŒ€์žฅ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์ข…ํ•ฉ ์ฝ”๋””๋„ค์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ๊ฐ ์ „๋ฌธ๊ฐ€์˜ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ Executive Summary๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
62
+ "document_analyst": {"name": "๐Ÿ“‹ ๋ฌธ์„œ ๋ถ„์„๊ฐ€", "role": "๋ฌธ์„œ/๊ณ„์•ฝ์„œ ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ๋ฌธ์„œ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ, ์œ ์ง€๋ณด์ˆ˜ ๊ณ„์•ฝ ๋“ฑ์„ ์ •๋ฐ€ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
63
+ "financial_expert": {"name": "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€", "role": "๋น„์šฉ/์ˆ˜์ต ๋ถ„์„", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์žฌ๋ฌด ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์žฌ๋ฌด์  ์˜ํ–ฅ์„ ๋ถ„์„ํ•˜๊ณ  ๋น„์šฉ ์ ˆ๊ฐ ๋ฐฉ์•ˆ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
64
+ "legal_advisor": {"name": "โš–๏ธ ๋ฒ•๋ฅ  ์ž๋ฌธ๊ฐ€", "role": "๋ฒ•์  ๋ฆฌ์Šคํฌ ๊ฒ€ํ† ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ๋ฒ•๋ฅ  ์ž๋ฌธ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ๋ฒ•์  ๋ฆฌ์Šคํฌ, ๋ถˆ๋ฆฌํ•œ ์กฐํ•ญ์„ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
65
+ "facility_manager": {"name": "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž", "role": "์šด์˜/์‹œ์„ค ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์‹œ์„ค ๊ด€๋ฆฌ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์‹œ์„ค ์šด์˜ ๊ด€์ ์—์„œ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."},
66
+ "market_analyst": {"name": "๐Ÿ“Š ์ƒ๊ถŒ ๋ถ„์„๊ฐ€", "role": "์ž…์ง€/์ƒ๊ถŒ ์ „๋ฌธ", "prompt": "๋‹น์‹ ์€ SOMA ํŒ€์˜ ์ƒ๊ถŒ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ž…์ง€, ์ฃผ๋ณ€ ์ƒ๊ถŒ, ์œ ๋™์ธ๊ตฌ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์‘๋‹ตํ•˜์„ธ์š”."}
67
+ }
68
+ SEOUL_DISTRICTS = {
69
+ "๊ฐ•๋‚จ๊ตฌ": {"lat": 37.5172, "lng": 127.0473, "ํŠน์„ฑ": "IT/๊ธˆ์œต ์ค‘์‹ฌ, ๊ณ ๊ธ‰ ์˜คํ”ผ์Šค", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 85000},
70
+ "์„œ์ดˆ๊ตฌ": {"lat": 37.4837, "lng": 127.0324, "ํŠน์„ฑ": "๋ฒ•์กฐํƒ€์šด, ๊ต์œก/๋ฌธํ™”", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 75000},
71
+ "์†กํŒŒ๊ตฌ": {"lat": 37.5145, "lng": 127.1050, "ํŠน์„ฑ": "์ž ์‹ค ์ƒ๊ถŒ, ์ฃผ๊ฑฐ/์ƒ์—… ๋ณตํ•ฉ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 65000},
72
+ "๋งˆํฌ๊ตฌ": {"lat": 37.5663, "lng": 126.9014, "ํŠน์„ฑ": "ํ™๋Œ€/ํ•ฉ์ • ์ƒ๊ถŒ, ๋ฌธํ™”/์˜ˆ์ˆ ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 55000},
73
+ "์˜๋“ฑํฌ๊ตฌ": {"lat": 37.5264, "lng": 126.8963, "ํŠน์„ฑ": "์—ฌ์˜๋„ ๊ธˆ์œต, ํƒ€์ž„์Šคํ€˜์–ด", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 70000},
74
+ "์šฉ์‚ฐ๊ตฌ": {"lat": 37.5324, "lng": 126.9903, "ํŠน์„ฑ": "์ดํƒœ์›/ํ•œ๋‚จ, ์žฌ๊ฐœ๋ฐœ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 60000},
75
+ "์„ฑ๋™๊ตฌ": {"lat": 37.5634, "lng": 127.0369, "ํŠน์„ฑ": "์„ฑ์ˆ˜๋™ ํ•ซํ”Œ, ์ง€์‹์‚ฐ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 55000},
76
+ "์ข…๋กœ๊ตฌ": {"lat": 37.5735, "lng": 126.9790, "ํŠน์„ฑ": "๋„์‹ฌ CBD, ์ „ํ†ต ์ƒ๊ถŒ", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 80000},
77
+ "์ค‘๊ตฌ": {"lat": 37.5641, "lng": 126.9979, "ํŠน์„ฑ": "๋ช…๋™/์„์ง€๋กœ, ๏ฟฝ๏ฟฝ๊ด‘/์ƒ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 90000},
78
+ "๊ฐ•์„œ๊ตฌ": {"lat": 37.5510, "lng": 126.8495, "ํŠน์„ฑ": "๋งˆ๊ณก์ง€๊ตฌ, ์‚ฐ์—…๋‹จ์ง€", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 45000},
79
+ "๊ตฌ๋กœ๊ตฌ": {"lat": 37.4954, "lng": 126.8874, "ํŠน์„ฑ": "๋””์ง€ํ„ธ๋‹จ์ง€, ์‚ฐ์—…", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 40000},
80
+ "๊ธˆ์ฒœ๊ตฌ": {"lat": 37.4569, "lng": 126.8956, "ํŠน์„ฑ": "๊ฐ€์‚ฐ๋””์ง€ํ„ธ, IT", "ํ‰๊ท ์ž„๋Œ€๋ฃŒ": 38000},
81
+ }
82
+ MARKET_REGIONS = {'์„œ์šธ': '์„œ์šธ_202506', '๊ฒฝ๊ธฐ': '๊ฒฝ๊ธฐ_202506', '๋ถ€์‚ฐ': '๋ถ€์‚ฐ_202506', '๋Œ€๊ตฌ': '๋Œ€๊ตฌ_202506', '์ธ์ฒœ': '์ธ์ฒœ_202506', '๊ด‘์ฃผ': '๊ด‘์ฃผ_202506', '๋Œ€์ „': '๋Œ€์ „_202506', '์šธ์‚ฐ': '์šธ์‚ฐ_202506', '์„ธ์ข…': '์„ธ์ข…_202506', '๊ฒฝ๋‚จ': '๊ฒฝ๋‚จ_202506', '๊ฒฝ๋ถ': '๊ฒฝ๋ถ_202506', '์ „๋‚จ': '์ „๋‚จ_202506', '์ „๋ถ': '์ „๋ถ_202506', '์ถฉ๋‚จ': '์ถฉ๋‚จ_202506', '์ถฉ๋ถ': '์ถฉ๋ถ_202506', '๊ฐ•์›': '๊ฐ•์›_202506', '์ œ์ฃผ': '์ œ์ฃผ_202506'}
83
+ class MarketDataLoader:
84
+ @staticmethod
85
+ def load_region_data(region: str, sample_size: int = 20000) -> pd.DataFrame:
86
+ if not DATASETS_AVAILABLE:
87
+ return pd.DataFrame()
88
+ try:
89
+ file_name = f"์†Œ์ƒ๊ณต์ธ์‹œ์žฅ์ง„ํฅ๊ณต๋‹จ_์ƒ๊ฐ€(์ƒ๊ถŒ)์ •๋ณด_{MARKET_REGIONS[region]}.csv"
90
+ dataset = load_dataset("ginipick/market", data_files=file_name, split="train")
91
+ df = dataset.to_pandas()
92
+ return df.sample(n=min(sample_size, len(df)), random_state=42)
93
+ except:
94
+ return pd.DataFrame()
95
+ @staticmethod
96
+ def load_multiple_regions(regions: List[str], sample_per_region: int = 20000) -> pd.DataFrame:
97
+ dfs = [MarketDataLoader.load_region_data(r, sample_per_region) for r in regions]
98
+ 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()
99
+ class MarketAnalyzer:
100
+ def __init__(self, df: pd.DataFrame):
101
+ self.df = df
102
+ self.prepare_data()
103
+ def prepare_data(self):
104
+ for col in ['๊ฒฝ๋„', '์œ„๋„']:
105
+ if col in self.df.columns:
106
+ self.df[col] = pd.to_numeric(self.df[col], errors='coerce')
107
+ self.df = self.df.dropna(subset=['๊ฒฝ๋„', '์œ„๋„'])
108
+ if '์ธต์ •๋ณด' in self.df.columns:
109
+ self.df['์ธต์ •๋ณด_์ˆซ์ž'] = self.df['์ธต์ •๋ณด'].apply(self._parse_floor)
110
+ def _parse_floor(self, floor_str):
111
+ if pd.isna(floor_str): return None
112
+ s = str(floor_str)
113
+ if '์ง€ํ•˜' in s or 'B' in s:
114
+ m = re.search(r'\d+', s)
115
+ return -int(m.group()) if m else -1
116
+ m = re.search(r'\d+', s)
117
+ return int(m.group()) if m else None
118
+ def get_summary(self) -> Dict:
119
+ return {
120
+ '์ด์ ํฌ์ˆ˜': len(self.df),
121
+ '์—…์ข…์ˆ˜': self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].nunique() if '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' in self.df.columns else 0,
122
+ '์ƒ์œ„์—…์ข…': self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].value_counts().head(5).to_dict() if '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' in self.df.columns else {},
123
+ }
124
+ def create_category_chart(self):
125
+ if not PLOTLY_AVAILABLE or '์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…' not in self.df.columns: return None
126
+ top = self.df['์ƒ๊ถŒ์—…์ข…์ค‘๋ถ„๋ฅ˜๋ช…'].value_counts().head(15)
127
+ fig = px.bar(x=top.values, y=top.index, orientation='h', title='๐Ÿ† ์ƒ์œ„ ์—…์ข… TOP 15', color=top.values, color_continuous_scale='blues')
128
+ fig.update_layout(showlegend=False, height=450)
129
+ return fig
130
+ def create_district_chart(self):
131
+ if not PLOTLY_AVAILABLE or '์‹œ๊ตฐ๊ตฌ๋ช…' not in self.df.columns: return None
132
+ counts = self.df['์‹œ๊ตฐ๊ตฌ๋ช…'].value_counts().head(15)
133
+ fig = px.bar(x=counts.values, y=counts.index, orientation='h', title='๐Ÿ“ ์ง€์—ญ๋ณ„ ์ ํฌ ๋ฐ€์ง‘๋„', color=counts.values, color_continuous_scale='reds')
134
+ fig.update_layout(showlegend=False, height=450)
135
+ return fig
136
+ def create_heatmap(self, sample_size: int = 2000) -> str:
137
+ if not FOLIUM_AVAILABLE: return "<p>folium ์„ค์น˜ ํ•„์š”</p>"
138
+ df_sample = self.df.sample(n=min(sample_size, len(self.df)), random_state=42)
139
+ m = folium.Map(location=[df_sample['์œ„๋„'].mean(), df_sample['๊ฒฝ๋„'].mean()], zoom_start=11, tiles='cartodbpositron')
140
+ HeatMap([[r['์œ„๋„'], r['๊ฒฝ๋„']] for _, r in df_sample.iterrows()], radius=15, blur=25).add_to(m)
141
+ return m._repr_html_()
142
+ class AppState:
143
+ def __init__(self):
144
+ self.analyzer = None
145
+ app_state = AppState()
146
+ def encode_image_to_base64(image_path: str) -> str:
147
+ with open(image_path, "rb") as f:
148
+ return base64.b64encode(f.read()).decode('utf-8')
149
+ def get_image_mime_type(file_path: str) -> str:
150
+ ext = file_path.lower().split('.')[-1]
151
+ return {'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'webp': 'image/webp'}.get(ext, 'image/jpeg')
152
+ def extract_text_from_image_fireworks(image_path: str) -> str:
153
+ """Fireworks AI Vision API๋ฅผ ์‚ฌ์šฉํ•œ ์ด๋ฏธ์ง€ OCR"""
154
+ if not FIREWORKS_API_KEY:
155
+ return """[๋ฐ๋ชจ ๋ชจ๋“œ] ์ด๋ฏธ์ง€ OCR ๊ฒฐ๊ณผ:
156
+ ---
157
+ ์ž„๋Œ€์ฐจ ๊ณ„์•ฝ์„œ
158
+ ์ œ1์กฐ (๋ชฉ์ ) ์ž„๋Œ€์ธ์€ ๏ฟฝ๏ฟฝ๋ž˜ ๋ถ€๋™์‚ฐ์„ ์ž„์ฐจ์ธ์—๊ฒŒ ์ž„๋Œ€ํ•œ๋‹ค.
159
+ ์†Œ์žฌ์ง€: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123, 5์ธต
160
+ ์ž„๋Œ€๋ฉด์ : 330.58ใŽก (100ํ‰)
161
+ ์ž„๋Œ€๊ธฐ๊ฐ„: 2024.01.01 ~ 2026.12.31 (3๋…„)
162
+ ๋ณด์ฆ๊ธˆ: ๊ธˆ ์‚ผ์–ต์›์ • (โ‚ฉ300,000,000)
163
+ ์›”์ž„๋Œ€๋ฃŒ: ๊ธˆ ์ผ์ฒœ์˜ค๋ฐฑ๋งŒ์›์ • (โ‚ฉ15,000,000)
164
+ ๊ด€๋ฆฌ๋น„: ํ‰๋‹น 25,000์› (์›” 250๋งŒ์›)
165
+ ---
166
+ โš ๏ธ FIREWORKS_API_KEY ์„ค์ • ์‹œ ์‹ค์ œ OCR ๊ฒฐ๊ณผ ์ œ๊ณต"""
167
+ try:
168
+ base64_image = encode_image_to_base64(image_path)
169
+ mime_type = get_image_mime_type(image_path)
170
+ headers = {
171
+ "Accept": "application/json",
172
+ "Content-Type": "application/json",
173
+ "Authorization": f"Bearer {FIREWORKS_API_KEY}"
174
+ }
175
+ payload = {
176
+ "model": FIREWORKS_VISION_MODEL,
177
+ "max_tokens": 4096,
178
+ "temperature": 0.2,
179
+ "messages": [
180
+ {
181
+ "role": "user",
182
+ "content": [
183
+ {
184
+ "type": "text",
185
+ "text": """์ด ์ด๋ฏธ์ง€๋Š” ๋ถ€๋™์‚ฐ ๊ด€๋ จ ๋ฌธ์„œ(๊ณ„์•ฝ์„œ, ๋„๋ฉด, ๊ด€๋ฆฌ๋ฌธ์„œ ๋“ฑ)์ž…๋‹ˆ๋‹ค.
186
+ ์ด๋ฏธ์ง€์— ์žˆ๋Š” ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”.
187
+ ํ‘œ๋‚˜ ๋„ํ‘œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜์—ฌ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ์„ธ์š”.
188
+ ํ•œ๊ตญ์–ด์™€ ์˜์–ด ๋ชจ๋‘ ์ •ํ™•ํ•˜๊ฒŒ ์ธ์‹ํ•ด์ฃผ์„ธ์š”.
189
+ ์ถ”์ถœํ•œ ํ…์ŠคํŠธ๋งŒ ์ถœ๋ ฅํ•˜๊ณ , ๋‹ค๋ฅธ ์„ค๋ช…์€ ํ•˜์ง€ ๋งˆ์„ธ์š”."""
190
+ },
191
+ {
192
+ "type": "image_url",
193
+ "image_url": {
194
+ "url": f"data:{mime_type};base64,{base64_image}"
195
+ }
196
+ }
197
+ ]
198
+ }
199
+ ]
200
+ }
201
+ response = requests.post(FIREWORKS_VISION_URL, headers=headers, json=payload, timeout=60)
202
+ if response.status_code == 200:
203
+ result = response.json()
204
+ content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
205
+ if content:
206
+ if "<think>" in content and "</think>" in content:
207
+ content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL).strip()
208
+ return content
209
+ return "โš ๏ธ OCR ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."
210
+ else:
211
+ return f"โŒ API ์˜ค๋ฅ˜ ({response.status_code}): {response.text[:200]}"
212
+ except requests.exceptions.Timeout:
213
+ return "โŒ API ์‘๋‹ต ์‹œ๊ฐ„ ์ดˆ๊ณผ. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."
214
+ except Exception as e:
215
+ return f"โŒ OCR ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {str(e)}"
216
+ def extract_text_from_pdf(file_path: str) -> str:
217
+ if not PDF_AVAILABLE:
218
+ return "โŒ PyMuPDF ์„ค์น˜ ํ•„์š”: pip install pymupdf"
219
+ try:
220
+ doc = fitz.open(file_path)
221
+ texts = [f"--- ํŽ˜์ด์ง€ {i+1} ---\n{page.get_text()}" for i, page in enumerate(doc) if page.get_text().strip()]
222
+ doc.close()
223
+ return "\n\n".join(texts) if texts else "โš ๏ธ ํ…์ŠคํŠธ ์ถ”์ถœ ์‹คํŒจ (์ด๋ฏธ์ง€ PDF์ผ ์ˆ˜ ์žˆ์Œ)"
224
+ except Exception as e:
225
+ return f"โŒ PDF ์˜ค๋ฅ˜: {e}"
226
+ def generate_response_fireworks(message: str, history: list, system_prompt: str) -> Generator:
227
+ """Fireworks AI๋ฅผ ์‚ฌ์šฉํ•œ ํ…์ŠคํŠธ ์ƒ์„ฑ (์ŠคํŠธ๋ฆฌ๋ฐ)"""
228
+ if not FIREWORKS_API_KEY:
229
+ demo = f"""## ๐Ÿข PMAI ๋ถ„์„ ๊ฒฐ๊ณผ
230
+ **์งˆ๋ฌธ**: {message}
231
+ ---
232
+ ### ๐Ÿ“‹ ๋ถ„์„ ๋‚ด์šฉ
233
+ 1. **ํ˜„ํ™ฉ**: ์ž…๋ ฅ ๋‚ด์šฉ ๊ธฐ๋ฐ˜ ์ž์‚ฐ๊ด€๋ฆฌ ๊ด€์  ๋ถ„์„ ์™„๋ฃŒ
234
+ 2. **ํ•ต์‹ฌ**: Zero Hardware ์ ‘๊ทผ, ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ๋น„์šฉ ์ ˆ๊ฐ ํฌ์ธํŠธ ๋„์ถœ
235
+ 3. **๊ถŒ์žฅ**: ์šด์˜๋น„์šฉ ๊ตฌ์กฐ ์žฌ๊ฒ€ํ† , ์—๋„ˆ์ง€ ํšจ์œจํ™”, ๊ณต์‹ค๋ฅ  ๊ด€๋ฆฌ ์ „๋žต
236
+ > โš ๏ธ ๋ฐ๋ชจ ๋ชจ๋“œ - FIREWORKS_API_KEY ์„ค์ • ์‹œ ์ƒ์„ธ ๋ถ„์„ ์ œ๊ณต"""
237
+ for i in range(0, len(demo), 20):
238
+ yield demo[:i+20]
239
+ return
240
+ messages = [{"role": "system", "content": system_prompt}]
241
+ for h in history:
242
+ if isinstance(h, (list, tuple)) and len(h) >= 2:
243
+ messages.extend([{"role": "user", "content": str(h[0])}, {"role": "assistant", "content": str(h[1])}])
244
+ messages.append({"role": "user", "content": message})
245
+ headers = {"Accept": "application/json", "Content-Type": "application/json", "Authorization": f"Bearer {FIREWORKS_API_KEY}"}
246
+ payload = {"model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507", "max_tokens": 4096, "temperature": 0.7, "stream": True, "messages": messages}
247
+ try:
248
+ response = requests.post(FIREWORKS_VISION_URL, headers=headers, json=payload, stream=True, timeout=60)
249
+ if response.status_code == 200:
250
+ full_response = ""
251
+ for line in response.iter_lines():
252
+ if line:
253
+ line_text = line.decode('utf-8')
254
+ if line_text.startswith('data: '):
255
+ data_str = line_text[6:]
256
+ if data_str.strip() == '[DONE]':
257
+ break
258
+ try:
259
+ data = json.loads(data_str)
260
+ content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
261
+ if content:
262
+ full_response += content
263
+ clean = re.sub(r'<think>.*?</think>', '', full_response, flags=re.DOTALL).strip()
264
+ yield clean
265
+ except:
266
+ continue
267
+ else:
268
+ yield f"โŒ API ์˜ค๋ฅ˜: {response.status_code}"
269
+ except Exception as e:
270
+ yield f"โŒ ์˜ค๋ฅ˜: {e}"
271
+ def generate_response(message: str, history: list, system_prompt: str = PMAI_SYSTEM_PROMPT) -> Generator:
272
+ """Groq ๋˜๋Š” Fireworks๋ฅผ ์‚ฌ์šฉํ•œ ์‘๋‹ต ์ƒ์„ฑ"""
273
+ if groq_client:
274
+ messages = [{"role": "system", "content": system_prompt}]
275
+ for h in history:
276
+ if isinstance(h, (list, tuple)) and len(h) >= 2:
277
+ messages.extend([{"role": "user", "content": str(h[0])}, {"role": "assistant", "content": str(h[1])}])
278
+ messages.append({"role": "user", "content": message})
279
+ try:
280
+ completion = groq_client.chat.completions.create(model="llama-3.3-70b-versatile", messages=messages, temperature=0.7, max_tokens=4096, stream=True)
281
+ response = ""
282
+ for chunk in completion:
283
+ if chunk.choices[0].delta.content:
284
+ response += chunk.choices[0].delta.content
285
+ yield response
286
+ return
287
+ except:
288
+ pass
289
+ yield from generate_response_fireworks(message, history, system_prompt)
290
+ def chat_respond(message: str, history: list):
291
+ if not message or not message.strip():
292
+ yield history or []
293
+ return
294
+ history = history or []
295
+ history_api = []
296
+ for h in history:
297
+ if isinstance(h, dict):
298
+ r, c = h.get("role", ""), h.get("content", "")
299
+ if r == "user": history_api.append([c, ""])
300
+ elif r == "assistant" and history_api: history_api[-1][1] = c
301
+ new_history = list(history) + [{"role": "user", "content": message}, {"role": "assistant", "content": ""}]
302
+ for chunk in generate_response(message, history_api, PMAI_SYSTEM_PROMPT):
303
+ new_history[-1] = {"role": "assistant", "content": chunk}
304
+ yield new_history
305
+ def run_soma_analysis(document_text: str, selected_agents: List[str]) -> Generator:
306
+ if not document_text.strip():
307
+ yield "๐Ÿ“„ ๋ฌธ์„œ ๋‚ด์šฉ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
308
+ return
309
+ if not selected_agents:
310
+ selected_agents = ["document_analyst", "financial_expert", "legal_advisor"]
311
+ output = "# ๐Ÿค– SOMA ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ํ˜‘์—… ๋ถ„์„\n\n---\n\n"
312
+ yield output
313
+ results = {}
314
+ for key in selected_agents:
315
+ agent = SOMA_AGENTS.get(key)
316
+ if not agent: continue
317
+ output += f"## {agent['name']}\n**์—ญํ• **: {agent['role']}\n\nโณ ๋ถ„์„ ์ค‘...\n\n"
318
+ yield output
319
+ prompt = f"{agent['prompt']}\n\n๋ถ„์„ํ•  ๋ฌธ์„œ:\n---\n{document_text[:6000]}\n---\n๊ตฌ์ฒด์ ์ธ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”."
320
+ agent_response = ""
321
+ for chunk in generate_response(prompt, [], agent['prompt']):
322
+ agent_response = chunk
323
+ results[key] = agent_response
324
+ output = output.replace("โณ ๋ถ„์„ ์ค‘...\n\n", f"{agent_response}\n\n---\n\n")
325
+ yield output
326
+ if len(results) > 1 and "coordinator" not in selected_agents:
327
+ output += f"## {SOMA_AGENTS['coordinator']['name']}\nโณ ์ข…ํ•ฉ ๋ถ„์„ ์ค‘...\n\n"
328
+ yield output
329
+ summary_prompt = f"๊ฐ ์ „๋ฌธ๊ฐ€ ๋ถ„์„์„ ์ข…ํ•ฉํ•˜์—ฌ Executive Summary๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”:\n" + "\n".join([f"### {SOMA_AGENTS[k]['name']}:\n{v[:1000]}" for k, v in results.items()])
330
+ coord_response = ""
331
+ for chunk in generate_response(summary_prompt, [], SOMA_AGENTS['coordinator']['prompt']):
332
+ coord_response = chunk
333
+ output = output.replace("โณ ์ข…ํ•ฉ ๋ถ„์„ ์ค‘...\n\n", f"{coord_response}\n\n")
334
+ yield output
335
+ yield output + "\nโœ… **SOMA ๋ถ„์„ ์™„๋ฃŒ**"
336
+ def analyze_document(document_text: str, document_type: str, file_upload: Optional[str] = None) -> Generator:
337
+ if file_upload:
338
+ ext = file_upload.lower().split('.')[-1]
339
+ if ext == 'pdf':
340
+ yield "๐Ÿ“„ PDF ํ…์ŠคํŠธ ์ถ”์ถœ ์ค‘..."
341
+ extracted = extract_text_from_pdf(file_upload)
342
+ if extracted.startswith("โŒ") or extracted.startswith("โš ๏ธ"):
343
+ yield extracted
344
+ return
345
+ document_text = extracted
346
+ yield f"๐Ÿ“„ PDF ์ถ”์ถœ ์™„๋ฃŒ ({len(extracted):,}์ž)\n\n๋ถ„์„ ์ค‘..."
347
+ elif ext in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
348
+ yield f"๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ OCR ์ฒ˜๋ฆฌ ์ค‘... (Fireworks Vision AI)"
349
+ extracted = extract_text_from_image_fireworks(file_upload)
350
+ if extracted.startswith("โŒ"):
351
+ yield extracted
352
+ return
353
+ document_text = extracted
354
+ yield f"๐Ÿ–ผ๏ธ OCR ์™„๋ฃŒ ({len(extracted):,}์ž)\n\n**์ถ”์ถœ๋œ ํ…์Šค๏ฟฝ๏ฟฝ๏ฟฝ:**\n```\n{extracted[:2000]}{'...' if len(extracted) > 2000 else ''}\n```\n\n๋ถ„์„ ์ค‘..."
355
+ else:
356
+ yield f"โŒ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: .{ext}"
357
+ return
358
+ if not document_text or not document_text.strip():
359
+ yield "๐Ÿ“„ ๋ฌธ์„œ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”."
360
+ return
361
+ prompt = f"""๋‹ค์Œ {document_type} ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:
362
+ ---
363
+ {document_text[:8000]}
364
+ ---
365
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„:
366
+ ## ๐Ÿ“‹ ๋ฌธ์„œ ์š”์•ฝ (3์ค„)
367
+ ## ๐Ÿ” ํ•ต์‹ฌ ์ •๋ณด
368
+ ## โš ๏ธ ๋ฆฌ์Šคํฌ ํฌ์ธํŠธ
369
+ ## ๐Ÿ’ก ์ตœ์ ํ™” ์ œ์•ˆ
370
+ ## ๐Ÿ“Š ์•ก์…˜ ์•„์ดํ…œ"""
371
+ full_output = ""
372
+ for chunk in generate_response(prompt, [], DOCUMENT_ANALYSIS_PROMPT):
373
+ full_output = chunk
374
+ yield full_output
375
+ full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
376
+ yield full_output
377
+ mini_agents = [("legal_advisor", "โš–๏ธ ๋ฒ•๋ฅ  ์ž๋ฌธ๊ฐ€"), ("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€")]
378
+ for agent_key, agent_label in mini_agents:
379
+ agent = SOMA_AGENTS.get(agent_key)
380
+ if not agent: continue
381
+ full_output += f"### {agent_label}\n"
382
+ yield full_output + "๋ถ„์„ ์ค‘...\n"
383
+ mini_prompt = f"{agent['prompt']}\n\n๋ฌธ์„œ ๋‚ด์šฉ:\n{document_text[:3000]}\n\nํ•ต์‹ฌ ํฌ์ธํŠธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”. ๊ฐ ํฌ์ธํŠธ๋Š” 1-2๋ฌธ์žฅ์œผ๋กœ."
384
+ agent_response = ""
385
+ for chunk in generate_response(mini_prompt, [], agent['prompt']):
386
+ agent_response = chunk
387
+ full_output += f"{agent_response}\n\n"
388
+ yield full_output
389
+ def analyze_cost(building_name, monthly_rent, maintenance, utility, personnel, repair, other, vacancy_rate, additional_info) -> Generator:
390
+ total = maintenance + utility + personnel + repair + other
391
+ noi = monthly_rent * (1 - vacancy_rate/100) - total
392
+ cost_data = f"""๊ฑด๋ฌผ๋ช…: {building_name}
393
+ ์›” ์ž„๋Œ€์ˆ˜์ž…: {monthly_rent:,.0f}์› | ๊ณต์‹ค๋ฅ : {vacancy_rate}%
394
+ ์šด์˜๋น„์šฉ ๋‚ด์—ญ:
395
+ - ๊ด€๋ฆฌ๋น„: {maintenance:,.0f}์› ({maintenance/total*100:.1f}%)
396
+ - ์œ ํ‹ธ๋ฆฌํ‹ฐ: {utility:,.0f}์› ({utility/total*100:.1f}%)
397
+ - ์ธ๊ฑด๋น„: {personnel:,.0f}์› ({personnel/total*100:.1f}%)
398
+ - ์ˆ˜์„ ์œ ์ง€๋น„: {repair:,.0f}์› ({repair/total*100:.1f}%)
399
+ - ๊ธฐํƒ€: {other:,.0f}์› ({other/total*100:.1f}%)
400
+ - ์ด ์šด์˜๋น„์šฉ: {total:,.0f}์›
401
+ - ์ˆœ์šด์˜์ˆ˜์ต(NOI): {noi:,.0f}์›
402
+ ์ถ”๊ฐ€์ •๋ณด: {additional_info or '์—†์Œ'}"""
403
+ prompt = f"""๊ฑด๋ฌผ ์šด์˜๋น„์šฉ์„ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:
404
+ {cost_data}
405
+ ๋ถ„์„ ํ˜•์‹:
406
+ ## ๐Ÿ“Š ๋น„์šฉ ๊ตฌ์กฐ ๋ถ„์„
407
+ ## ๐Ÿ”ด ๋ˆ„์ˆ˜ ํฌ์ธํŠธ ์‹๋ณ„
408
+ ## ๐Ÿ’ฐ ์ ˆ๊ฐ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ (๊ตฌ์ฒด์  ๊ธˆ์•ก ์ œ์‹œ)
409
+ ## ๐Ÿ“ˆ ROI ๊ธฐ๋ฐ˜ ์šฐ์„ ์ˆœ์œ„
410
+ ## ๐Ÿ’ต ์˜ˆ์ƒ ์ ˆ๊ฐ ํšจ๊ณผ (์›”๊ฐ„/์—ฐ๊ฐ„)"""
411
+ full_output = ""
412
+ for chunk in generate_response(prompt, [], COST_ANALYSIS_PROMPT):
413
+ full_output = chunk
414
+ yield full_output
415
+ full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
416
+ yield full_output
417
+ mini_agents = [("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€"), ("facility_manager", "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž")]
418
+ for agent_key, agent_label in mini_agents:
419
+ agent = SOMA_AGENTS.get(agent_key)
420
+ if not agent: continue
421
+ full_output += f"### {agent_label}\n"
422
+ yield full_output + "๋ถ„์„ ์ค‘...\n"
423
+ mini_prompt = f"{agent['prompt']}\n\n๋น„์šฉ ๋ฐ์ดํ„ฐ:\n{cost_data}\n\n๋‹น์‹ ์˜ ์ „๋ฌธ ๋ถ„์•ผ ๊ด€์ ์—์„œ ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ œ์‹œํ•ด์ฃผ์„ธ์š”."
424
+ agent_response = ""
425
+ for chunk in generate_response(mini_prompt, [], agent['prompt']):
426
+ agent_response = chunk
427
+ full_output += f"{agent_response}\n\n"
428
+ yield full_output
429
+ def create_seoul_map(selected: str = None) -> str:
430
+ if not FOLIUM_AVAILABLE:
431
+ return "<div style='padding:40px;text-align:center;'><p>folium ์„ค์น˜ ํ•„์š”</p></div>"
432
+ m = folium.Map(location=[37.5665, 126.9780], zoom_start=11, tiles='cartodbpositron')
433
+ for name, info in SEOUL_DISTRICTS.items():
434
+ color = 'red' if name == selected else 'blue'
435
+ popup = f"<b>{name}</b><br>{info['ํŠน์„ฑ']}<br>์ž„๋Œ€๋ฃŒ: {info['ํ‰๊ท ์ž„๋Œ€๋ฃŒ']:,}์›/ํ‰"
436
+ folium.Marker([info['lat'], info['lng']], popup=popup, tooltip=name, icon=folium.Icon(color=color, icon='building', prefix='fa')).add_to(m)
437
+ if name == selected:
438
+ folium.Circle([info['lat'], info['lng']], radius=1500, color='red', fill=True, fillOpacity=0.2).add_to(m)
439
+ return m._repr_html_()
440
+ def analyze_location(district: str) -> Generator:
441
+ if district not in SEOUL_DISTRICTS:
442
+ yield "์ง€์—ญ ์ •๋ณด ์—†์Œ"
443
+ return
444
+ info = SEOUL_DISTRICTS[district]
445
+ location_data = f"""์ง€์—ญ: ์„œ์šธ์‹œ {district}
446
+ ํŠน์„ฑ: {info['ํŠน์„ฑ']}
447
+ ํ‰๊ท  ์ž„๋Œ€๋ฃŒ: {info['ํ‰๊ท ์ž„๋Œ€๋ฃŒ']:,}์›/ํ‰
448
+ ์œ„์น˜: ์œ„๋„ {info['lat']}, ๊ฒฝ๋„ {info['lng']}"""
449
+ prompt = f"""์„œ์šธ์‹œ {district}์˜ ์ƒ๊ถŒ ๋ฐ ๋ถ€๋™์‚ฐ ์ž…์ง€๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
450
+ {location_data}
451
+ ๋‹ค์Œ ๊ด€์ ์—์„œ ์ƒ์„ธํžˆ ๋ถ„์„:
452
+ ## ๐Ÿ“ ์ƒ๊ถŒ ํŠน์„ฑ ๋ถ„์„
453
+ (์ฃผ์š” ์—…์ข…, ์œ ๋™์ธ๊ตฌ ํŒจํ„ด, ์†Œ๋น„ ํŠน์„ฑ)
454
+ ## ๐Ÿข ์˜คํ”ผ์Šค/์ƒ๊ฐ€ ์‹œ์žฅ ํ˜„ํ™ฉ
455
+ (์ž„๋Œ€๋ฃŒ ์ˆ˜์ค€, ๊ณต์‹ค๋ฅ  ์ถ”์ด, ์ˆ˜์š”-๊ณต๊ธ‰)
456
+ ## ๐Ÿ“ˆ ํˆฌ์ž ๋งค๋ ฅ๋„ ํ‰๊ฐ€
457
+ (์ž์‚ฐ๊ฐ€์น˜ ์ƒ์Šน ๊ฐ€๋Šฅ์„ฑ, ๊ฐœ๋ฐœ ํ˜ธ์žฌ)
458
+ ## โš ๏ธ ๋ฆฌ์Šคํฌ ์š”์ธ
459
+ ## ๐ŸŽฏ ์ถ”์ฒœ ์ „๋žต"""
460
+ full_output = ""
461
+ for chunk in generate_response(prompt, [], SOMA_AGENTS['market_analyst']['prompt']):
462
+ full_output = chunk
463
+ yield full_output
464
+ full_output += "\n\n---\n\n## ๐Ÿค– SOMA ์ „๋ฌธ๊ฐ€ ์ถ”๊ฐ€ ์ธ์‚ฌ์ดํŠธ\n\n"
465
+ yield full_output
466
+ mini_agents = [("financial_expert", "๐Ÿ’ฐ ์žฌ๋ฌด ์ „๋ฌธ๊ฐ€"), ("facility_manager", "๐Ÿ”ง ์‹œ์„ค ๊ด€๋ฆฌ์ž")]
467
+ for agent_key, agent_label in mini_agents:
468
+ agent = SOMA_AGENTS.get(agent_key)
469
+ if not agent: continue
470
+ full_output += f"### {agent_label}\n"
471
+ yield full_output + "๋ถ„์„ ์ค‘...\n"
472
+ mini_prompt = f"{agent['prompt']}\n\n์ž…์ง€ ์ •๋ณด:\n{location_data}\n\n์ด ์ง€์—ญ์—์„œ ์ž์‚ฐ๊ด€๋ฆฌ ์‹œ ๋‹น์‹ ์˜ ์ „๋ฌธ ๋ถ„์•ผ ๊ด€์ ์—์„œ ํ•ต์‹ฌ ์กฐ์–ธ 3๊ฐ€์ง€๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ œ์‹œํ•ด์ฃผ์„ธ์š”."
473
+ agent_response = ""
474
+ for chunk in generate_response(mini_prompt, [], agent['prompt']):
475
+ agent_response = chunk
476
+ full_output += f"{agent_response}\n\n"
477
+ yield full_output
478
+ def create_cost_chart(m, u, p, r, o):
479
+ if not PLOTLY_AVAILABLE: return None
480
+ fig = go.Figure(data=[go.Pie(labels=['๊ด€๋ฆฌ๋น„','์œ ํ‹ธ๋ฆฌํ‹ฐ','์ธ๊ฑด๋น„','์ˆ˜์„ ๋น„','๊ธฐํƒ€'], values=[m,u,p,r,o], hole=0.4, marker_colors=['#3B82F6','#10B981','#F59E0B','#EF4444','#8B5CF6'])])
481
+ fig.update_layout(title='์›”๊ฐ„ ์šด์˜๋น„์šฉ', height=350, paper_bgcolor='#ffffff', font=dict(color='#1e293b'))
482
+ return fig
483
+ CSS = """
484
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap');
485
+ .gradio-container { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #f1f5f9 100%) !important; font-family: 'Noto Sans KR', sans-serif !important; min-height: 100vh; }
486
+ .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; }
487
+ .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; }
488
+ .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; }
489
+ textarea, input[type="text"], input[type="number"] { background: #ffffff !important; border: 2px solid #e2e8f0 !important; color: #1e293b !important; border-radius: 10px !important; }
490
+ textarea:focus, input:focus { border-color: #3b82f6 !important; box-shadow: 0 0 0 3px rgba(59,130,246,0.15) !important; }
491
+ label, .gr-input-label { color: #334155 !important; font-weight: 500 !important; }
492
+ .gr-markdown { color: #334155 !important; }
493
+ .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { color: #1e40af !important; font-weight: 700 !important; }
494
+ .gr-markdown h1 { font-size: 1.8em !important; }
495
+ .gr-markdown h2 { font-size: 1.4em !important; }
496
+ .gr-markdown h3 { font-size: 1.2em !important; }
497
+ .gr-chatbot { background: #ffffff !important; border: 1px solid #e2e8f0 !important; border-radius: 16px !important; }
498
+ .gr-tab-nav { background: #f1f5f9 !important; border-radius: 12px !important; padding: 4px !important; }
499
+ .gr-tab-nav button { color: #64748b !important; font-weight: 500 !important; border-radius: 8px !important; }
500
+ .gr-tab-nav button.selected { background: #ffffff !important; color: #2563eb !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; }
501
+ .gr-accordion { background: #f8fafc !important; border: 1px solid #e2e8f0 !important; border-radius: 12px !important; }
502
+ .gr-dropdown { background: #ffffff !important; border: 2px solid #e2e8f0 !important; border-radius: 10px !important; }
503
+ .gr-checkbox-group { background: #f8fafc !important; border-radius: 10px !important; padding: 12px !important; }
504
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
505
+ ::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
506
+ ::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 4px; }
507
+ ::-webkit-scrollbar-thumb:hover { background: #64748b; }
508
+ footer { display: none !important; }
509
+ """
510
+ def create_demo():
511
+ with gr.Blocks(title="TenAI PMAI Pro", css=CSS) as demo:
512
+ 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);">
513
+ <h1 style="color:#1e40af;font-size:2.6em;margin:0;font-weight:800;">๐Ÿข TenAI PMAI Pro</h1>
514
+ <p style="color:#475569;margin:12px 0 5px 0;font-size:1.15em;font-weight:500;">๊ธฐ์—… ์ž์‚ฐ๊ด€๋ฆฌ AI ํ”Œ๋žซํผ</p>
515
+ <p style="color:#64748b;margin:5px 0 20px 0;font-size:0.95em;">"ํ•˜๋“œ์›จ์–ด ์—†๋Š” ๊ฑด๋ฌผ ์šด์˜์ฒด์ œ(OS), TenAI"</p>
516
+ <div style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;">
517
+ <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>
518
+ <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>
519
+ <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>
520
+ </div></div>""")
521
+ with gr.Tabs():
522
+ with gr.Tab("๐Ÿ’ฌ PMAI ์ƒ๋‹ด"):
523
+ gr.Markdown("### ๐Ÿค– 24์‹œ๊ฐ„ AI ์ž์‚ฐ๊ด€๋ฆฌ ๋น„์„œ")
524
+ chatbot = gr.Chatbot(height=400)
525
+ with gr.Row():
526
+ msg = gr.Textbox(placeholder="์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”...", show_label=False, scale=9)
527
+ btn = gr.Button("์ „์†ก", variant="primary", scale=1)
528
+ gr.Examples(["๊ณต์‹ค๋ฅ ์„ ๋‚ฎ์ถ”๋Š” ๋ฐฉ๋ฒ•์€?", "๊ฑด๋ฌผ ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ ์ ˆ๊ฐ ์ „๋žต", "์ž„๋Œ€์ฐจ ๊ณ„์•ฝ ๊ฐฑ์‹  ์‹œ ์ฃผ์˜์ "], inputs=msg)
529
+ msg.submit(chat_respond, [msg, chatbot], [chatbot]).then(lambda: "", None, [msg])
530
+ btn.click(chat_respond, [msg, chatbot], [chatbot]).then(lambda: "", None, [msg])
531
+ with gr.Tab("๐Ÿ“„ ๋ฌธ์„œ ๋ถ„์„"):
532
+ gr.Markdown("### ๐Ÿ“‹ Vision AI ๋ฌธ์„œ ๋ถ„์„\n**PDF** ๋˜๋Š” **์ด๋ฏธ์ง€**(๊ณ„์•ฝ์„œ ์‚ฌ์ง„, ์Šค์บ”๋ณธ) ์—…๋กœ๋“œ ์‹œ ์ž๋™ OCR ์ฒ˜๋ฆฌ")
533
+ with gr.Row():
534
+ with gr.Column(scale=1):
535
+ doc_type = gr.Dropdown(["์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ", "์œ ์ง€๋ณด์ˆ˜ ๋ฌธ์„œ", "์‹œ์„ค์ ๊ฒ€ ๋ณด๊ณ ์„œ", "๊ด€๋ฆฌ๋น„ ๋‚ด์—ญ์„œ", "๊ฑด๋ฌผ ๋„๋ฉด", "๊ธฐํƒ€"], value="์ž„๋Œ€์ฐจ๊ณ„์•ฝ์„œ", label="๋ฌธ์„œ ์œ ํ˜•")
536
+ file_upload = gr.File(label="๐Ÿ“Ž ํŒŒ์ผ ์—…๋กœ๋“œ (PDF/์ด๋ฏธ์ง€)", file_types=[".pdf",".jpg",".jpeg",".png",".gif",".webp"], type="filepath")
537
+ gr.Markdown("<small>โœ… ์ง€์›: PDF, JPG, PNG, GIF, WEBP | ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€๋Š” Fireworks Vision AI๋กœ OCR</small>")
538
+ doc_input = gr.Textbox(lines=6, placeholder="๋˜๋Š” ํ…์ŠคํŠธ ์ง์ ‘ ์ž…๋ ฅ...", label="๋ฌธ์„œ ๋‚ด์šฉ")
539
+ analyze_btn = gr.Button("๐Ÿ” ๋ถ„์„ ์‹œ์ž‘", variant="primary", size="lg")
540
+ with gr.Column(scale=1):
541
+ analysis_output = gr.Markdown("### ๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ\nํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ ํ›„ ๋ถ„์„ ์‹œ์ž‘")
542
+ analyze_btn.click(analyze_document, [doc_input, doc_type, file_upload], analysis_output)
543
+ with gr.Tab("๐Ÿ’ฐ ๋น„์šฉ ๋ถ„์„"):
544
+ gr.Markdown("### ๐Ÿ“ˆ ์šด์˜๋น„์šฉ ์ตœ์ ํ™” ๋ถ„์„")
545
+ with gr.Row():
546
+ with gr.Column():
547
+ building = gr.Textbox(label="๊ฑด๋ฌผ๋ช…", value="๊ฐ•๋‚จํ…Œํฌํƒ€์›Œ")
548
+ rent = gr.Number(label="์›” ์ž„๋Œ€์ˆ˜์ž… (์›)", value=50000000)
549
+ vacancy = gr.Slider(0, 100, 10, label="๊ณต์‹ค๋ฅ  (%)")
550
+ gr.Markdown("#### ์›”๊ฐ„ ์šด์˜๋น„์šฉ")
551
+ m_cost = gr.Number(label="๊ด€๋ฆฌ๋น„", value=5000000)
552
+ u_cost = gr.Number(label="์œ ํ‹ธ๋ฆฌํ‹ฐ", value=8000000)
553
+ p_cost = gr.Number(label="์ธ๊ฑด๋น„", value=12000000)
554
+ r_cost = gr.Number(label="์ˆ˜์„ ์œ ์ง€๋น„", value=3000000)
555
+ o_cost = gr.Number(label="๊ธฐํƒ€", value=2000000)
556
+ add_info = gr.Textbox(label="์ถ”๊ฐ€ ์ •๋ณด", lines=2)
557
+ cost_btn = gr.Button("๐Ÿ’ก ๋น„์šฉ ๋ถ„์„", variant="primary")
558
+ with gr.Column():
559
+ cost_chart = gr.Plot()
560
+ cost_output = gr.Markdown("### ๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ")
561
+ 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)
562
+ cost_btn.click(analyze_cost, [building,rent,m_cost,u_cost,p_cost,r_cost,o_cost,vacancy,add_info], cost_output)
563
+ with gr.Tab("๐Ÿ—บ๏ธ ์ž…์ง€ ๋ถ„์„"):
564
+ gr.Markdown("### ๐Ÿ“ ์„œ์šธ์‹œ ์ƒ๊ถŒ ๋ถ„์„")
565
+ with gr.Row():
566
+ with gr.Column():
567
+ district = gr.Dropdown(list(SEOUL_DISTRICTS.keys()), value="๊ฐ•๋‚จ๊ตฌ", label="์ง€์—ญ ์„ ํƒ")
568
+ loc_btn = gr.Button("๐Ÿ“Š ์ž…์ง€ ๋ถ„์„", variant="primary")
569
+ with gr.Column():
570
+ map_html = gr.HTML(value=create_seoul_map())
571
+ loc_output = gr.Markdown("### ๋ถ„์„ ๊ฒฐ๊ณผ")
572
+ district.change(create_seoul_map, [district], [map_html])
573
+ loc_btn.click(analyze_location, [district], loc_output)
574
+ with gr.Tab("๐Ÿค– SOMA ํ˜‘์—…"):
575
+ gr.Markdown("### ๐Ÿค– ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ํ˜‘์—… ๋ถ„์„\n6๋ช…๏ฟฝ๏ฟฝ AI ์ „๋ฌธ๊ฐ€๊ฐ€ ๋ฌธ์„œ๋ฅผ ๋‹ค๊ฐ๋„๋กœ ๋ถ„์„")
576
+ with gr.Row():
577
+ with gr.Column():
578
+ soma_agents = gr.CheckboxGroup([("๐Ÿ“‹ ๋ฌธ์„œ๋ถ„์„๊ฐ€","document_analyst"),("๐Ÿ’ฐ ์žฌ๋ฌด์ „๋ฌธ๊ฐ€","financial_expert"),("โš–๏ธ ๋ฒ•๋ฅ ์ž๋ฌธ๊ฐ€","legal_advisor"),("๐Ÿ”ง ์‹œ์„ค๊ด€๋ฆฌ์ž","facility_manager"),("๐Ÿ“Š ์ƒ๊ถŒ๋ถ„์„๊ฐ€","market_analyst")], value=["document_analyst","financial_expert","legal_advisor"], label="๋ถ„์„ ํŒ€")
579
+ soma_file = gr.File(label="ํŒŒ์ผ ์—…๋กœ๋“œ", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath")
580
+ soma_text = gr.Textbox(lines=5, placeholder="๋˜๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ...", label="๋ฌธ์„œ ๋‚ด์šฉ")
581
+ soma_btn = gr.Button("๐Ÿš€ SOMA ๋ถ„์„", variant="primary")
582
+ with gr.Column():
583
+ soma_output = gr.Markdown("### SOMA ๋ถ„์„ ๊ฒฐ๊ณผ")
584
+ def soma_with_file(text, agents, file):
585
+ doc = text or ""
586
+ if file:
587
+ ext = file.lower().split('.')[-1]
588
+ if ext == 'pdf':
589
+ doc = extract_text_from_pdf(file)
590
+ elif ext in ['jpg','jpeg','png','gif','webp']:
591
+ doc = extract_text_from_image_fireworks(file)
592
+ if doc.startswith("โŒ"):
593
+ yield doc
594
+ return
595
+ if not doc.strip():
596
+ yield "๋ฌธ์„œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
597
+ return
598
+ yield from run_soma_analysis(doc, agents)
599
+ soma_btn.click(soma_with_file, [soma_text, soma_agents, soma_file], soma_output)
600
+ with gr.Tab("โ„น๏ธ About"):
601
+ gr.Markdown("""
602
+ ## ๐Ÿข TenAI PMAI Pro
603
+ ### ๋น„์ „: "ํ•˜๋“œ์›จ์–ด ์—†๋Š” ๊ฑด๋ฌผ ์šด์˜์ฒด์ œ(OS)"
604
+ ### ํ•ต์‹ฌ ๊ธฐ๋Šฅ
605
+ | ๊ธฐ๋Šฅ | ์„ค๋ช… |
606
+ |-----|-----|
607
+ | ๐Ÿ–ผ๏ธ **Fireworks Vision AI** | ์ด๋ฏธ์ง€ OCR (Qwen3-VL-235B) |
608
+ | ๐Ÿ“„ **๋ฌธ์„œ ๋ถ„์„** | PDF/์ด๋ฏธ์ง€ ์ž๋™ ํ…์ŠคํŠธ ์ถ”์ถœ ๋ฐ ๋ถ„์„ |
609
+ | ๐Ÿค– **SOMA ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ** | 6๋ช… AI ์ „๋ฌธ๊ฐ€ ํ˜‘์—… |
610
+ | ๐Ÿ—บ๏ธ **์ƒ๊ถŒ ๋ถ„์„** | ์„œ์šธ์‹œ 12๊ฐœ ๊ตฌ ์ž…์ง€ ๋ถ„์„ |
611
+ ### API ์„ค์ •
612
+ ```
613
+ FIREWORKS_API_KEY=your_key # Vision AI + LLM
614
+ GROQ_API_KEY=your_key # ๋น ๋ฅธ LLM (์„ ํƒ)
615
+ ```
616
+ ### Contact
617
+ ๐Ÿ“ง ten@tenspace.co.kr | ๐Ÿ“ฑ 010-2710-6246
618
+ """)
619
+ 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>")
620
+ return demo
621
+ if __name__ == "__main__":
622
+ demo = create_demo()
623
+ demo.launch(server_name="0.0.0.0", server_port=7860)