""" 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 "

folium 설치 필요

" 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 "" in content and "" in content: content = re.sub(r'.*?', '', 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'.*?', '', 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 "

folium 설치 필요

" 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"{name}
{info['특성']}
임대료: {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("""

🏢 TenAI PMAI Pro

기업 자산관리 AI 플랫폼

"하드웨어 없는 건물 운영체제(OS), TenAI"

⚡ Zero Hardware 🖼️ Fireworks Vision AI 🤖 SOMA Multi-Agent
""") 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("✅ 지원: PDF, JPG, PNG, GIF, WEBP | 🖼️ 이미지는 Fireworks Vision AI로 OCR") 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("

🚀 Powered by Ten-AX Engine | Fireworks Vision AI | SOMA Multi-Agent

") return demo if __name__ == "__main__": demo = create_demo() demo.launch(server_name="0.0.0.0", server_port=7860)