aetherforge-platform 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aetherforge_platform-1.0.0.dist-info/METADATA +86 -0
- aetherforge_platform-1.0.0.dist-info/RECORD +55 -0
- aetherforge_platform-1.0.0.dist-info/WHEEL +5 -0
- aetherforge_platform-1.0.0.dist-info/top_level.txt +4 -0
- ai-life-assistant-copy/ai_agent.py +145 -0
- ai-life-assistant-copy/avatar_manager.py +231 -0
- ai-life-assistant-copy/avatar_packer.py +261 -0
- ai-life-assistant-copy/backup_all.py +262 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/ai_agent.py +145 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/avatar_manager.py +231 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/avatar_packer.py +261 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/backup_all.py +262 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/commands.py +210 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/config.py +30 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/daemon/__init__.py +3 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/daemon/daemon.py +174 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/database.py +292 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/graph.py +531 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/main.py +830 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/mcp_tools.py +449 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/memory.py +92 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/memory_v2.py +333 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/mock_shopping_data.py +172 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/personality.py +159 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/speech.py +41 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/test_simple.py +127 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tools/__init__.py +15 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tools/amazon_tool.py +103 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tools/calendar_tool.py +92 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tools/reminder_tool.py +92 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tools/weather_tool.py +45 -0
- ai-life-assistant-copy/backups/backup_20260404_193836/tree_memory.py +340 -0
- ai-life-assistant-copy/commands.py +210 -0
- ai-life-assistant-copy/config.py +30 -0
- ai-life-assistant-copy/daemon/__init__.py +3 -0
- ai-life-assistant-copy/daemon/daemon.py +174 -0
- ai-life-assistant-copy/database.py +292 -0
- ai-life-assistant-copy/graph.py +531 -0
- ai-life-assistant-copy/main.py +830 -0
- ai-life-assistant-copy/mcp_tools.py +449 -0
- ai-life-assistant-copy/memory.py +92 -0
- ai-life-assistant-copy/memory_v2.py +333 -0
- ai-life-assistant-copy/mock_shopping_data.py +172 -0
- ai-life-assistant-copy/personality.py +159 -0
- ai-life-assistant-copy/speech.py +41 -0
- ai-life-assistant-copy/test_simple.py +127 -0
- ai-life-assistant-copy/tools/__init__.py +15 -0
- ai-life-assistant-copy/tools/amazon_tool.py +103 -0
- ai-life-assistant-copy/tools/calendar_tool.py +92 -0
- ai-life-assistant-copy/tools/reminder_tool.py +92 -0
- ai-life-assistant-copy/tools/weather_tool.py +45 -0
- ai-life-assistant-copy/tree_memory.py +340 -0
- ai_agent_runtime.py +447 -0
- main.py +6752 -0
- mcp_server.py +427 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException, UploadFile, File
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
from fastapi.staticfiles import StaticFiles
|
|
4
|
+
from fastapi.responses import FileResponse
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from typing import List, Dict, Any, Optional
|
|
7
|
+
import os
|
|
8
|
+
from graph import run_chat
|
|
9
|
+
from config import APP_NAME, APP_VERSION, DEBUG
|
|
10
|
+
from personality import personality_analyzer
|
|
11
|
+
from ai_agent import agent_system
|
|
12
|
+
from speech import speech_recognition
|
|
13
|
+
from mcp_tools import mcp_tool_manager
|
|
14
|
+
from avatar_manager import avatar_manager
|
|
15
|
+
from avatar_packer import avatar_packer, marketplace
|
|
16
|
+
from tree_memory import tree_memory_system
|
|
17
|
+
from database import avatar_database
|
|
18
|
+
|
|
19
|
+
app = FastAPI(
|
|
20
|
+
title=APP_NAME,
|
|
21
|
+
version=APP_VERSION,
|
|
22
|
+
debug=DEBUG
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app.add_middleware(
|
|
26
|
+
CORSMiddleware,
|
|
27
|
+
allow_origins=["*"],
|
|
28
|
+
allow_credentials=True,
|
|
29
|
+
allow_methods=["*"],
|
|
30
|
+
allow_headers=["*"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
class ChatRequest(BaseModel):
|
|
34
|
+
message: str
|
|
35
|
+
user_id: Optional[str] = "default"
|
|
36
|
+
conversation_history: Optional[List[Dict[str, str]]] = None
|
|
37
|
+
|
|
38
|
+
class ChatResponse(BaseModel):
|
|
39
|
+
success: bool
|
|
40
|
+
results: List[Dict[str, Any]]
|
|
41
|
+
cards: List[Dict[str, Any]]
|
|
42
|
+
visualization: Optional[str]
|
|
43
|
+
intent: Optional[str]
|
|
44
|
+
status: str
|
|
45
|
+
limit_message: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
class PersonalityAnalysisRequest(BaseModel):
|
|
48
|
+
user_id: str
|
|
49
|
+
messages: List[Dict[str, str]]
|
|
50
|
+
|
|
51
|
+
class MatchRequest(BaseModel):
|
|
52
|
+
user1_id: str
|
|
53
|
+
user2_id: str
|
|
54
|
+
|
|
55
|
+
@app.get("/")
|
|
56
|
+
async def root():
|
|
57
|
+
index_path = os.path.join(os.path.dirname(__file__), "static", "index.html")
|
|
58
|
+
if os.path.exists(index_path):
|
|
59
|
+
return FileResponse(index_path)
|
|
60
|
+
return {
|
|
61
|
+
"name": APP_NAME,
|
|
62
|
+
"version": APP_VERSION,
|
|
63
|
+
"status": "running"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@app.get("/profile")
|
|
67
|
+
async def profile_page():
|
|
68
|
+
profile_path = os.path.join(os.path.dirname(__file__), "static", "profile.html")
|
|
69
|
+
if os.path.exists(profile_path):
|
|
70
|
+
return FileResponse(profile_path)
|
|
71
|
+
raise HTTPException(status_code=404, detail="Profile page not found")
|
|
72
|
+
|
|
73
|
+
@app.get("/test-products")
|
|
74
|
+
async def test_products_page():
|
|
75
|
+
test_path = os.path.join(os.path.dirname(__file__), "static", "test_products.html")
|
|
76
|
+
if os.path.exists(test_path):
|
|
77
|
+
return FileResponse(test_path)
|
|
78
|
+
raise HTTPException(status_code=404, detail="Test products page not found")
|
|
79
|
+
|
|
80
|
+
@app.get("/health")
|
|
81
|
+
async def health_check():
|
|
82
|
+
return {"status": "healthy"}
|
|
83
|
+
|
|
84
|
+
# ============================================
|
|
85
|
+
# Usage Tracking & Subscription System
|
|
86
|
+
# ============================================
|
|
87
|
+
|
|
88
|
+
# Usage tracking system
|
|
89
|
+
usage_stats = {
|
|
90
|
+
"default": {
|
|
91
|
+
"messages_sent": 0,
|
|
92
|
+
"tokens_used": 0,
|
|
93
|
+
"last_reset": "2025-01-01"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Demo subscription plans (for demonstration)
|
|
98
|
+
subscription_plans = {
|
|
99
|
+
"free": {
|
|
100
|
+
"name": "Free",
|
|
101
|
+
"daily_limit": 10,
|
|
102
|
+
"monthly_limit": 100,
|
|
103
|
+
"features": ["Basic chat", "10 messages/day"]
|
|
104
|
+
},
|
|
105
|
+
"pro": {
|
|
106
|
+
"name": "Pro",
|
|
107
|
+
"daily_limit": 100,
|
|
108
|
+
"monthly_limit": 3000,
|
|
109
|
+
"features": ["Advanced chat", "100 messages/day", "Priority support"]
|
|
110
|
+
},
|
|
111
|
+
"unlimited": {
|
|
112
|
+
"name": "Unlimited",
|
|
113
|
+
"daily_limit": None,
|
|
114
|
+
"monthly_limit": None,
|
|
115
|
+
"features": ["Unlimited chat", "All features", "Premium support"]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
current_plan = "free" # Demo default plan
|
|
120
|
+
|
|
121
|
+
@app.get("/api/usage")
|
|
122
|
+
async def get_usage(user_id: str = "default"):
|
|
123
|
+
"""Get usage statistics for a user"""
|
|
124
|
+
if user_id not in usage_stats:
|
|
125
|
+
usage_stats[user_id] = {
|
|
126
|
+
"messages_sent": 0,
|
|
127
|
+
"tokens_used": 0,
|
|
128
|
+
"last_reset": "2025-01-01"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"success": True,
|
|
133
|
+
"usage": usage_stats[user_id],
|
|
134
|
+
"plan": subscription_plans[current_plan],
|
|
135
|
+
"current_plan": current_plan
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@app.post("/api/usage/reset")
|
|
139
|
+
async def reset_usage(user_id: str = "default"):
|
|
140
|
+
"""Reset usage statistics (demo only)"""
|
|
141
|
+
if user_id in usage_stats:
|
|
142
|
+
usage_stats[user_id]["messages_sent"] = 0
|
|
143
|
+
usage_stats[user_id]["tokens_used"] = 0
|
|
144
|
+
|
|
145
|
+
return {"success": True, "message": "Usage reset"}
|
|
146
|
+
|
|
147
|
+
@app.get("/api/plans")
|
|
148
|
+
async def get_plans():
|
|
149
|
+
"""Get available subscription plans"""
|
|
150
|
+
return {
|
|
151
|
+
"success": True,
|
|
152
|
+
"plans": subscription_plans,
|
|
153
|
+
"current_plan": current_plan
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def check_limit(user_id: str) -> tuple[bool, str]:
|
|
157
|
+
"""
|
|
158
|
+
Check if user has exceeded their usage limit
|
|
159
|
+
Returns (allowed, message)
|
|
160
|
+
"""
|
|
161
|
+
if current_plan == "unlimited":
|
|
162
|
+
return (True, "")
|
|
163
|
+
|
|
164
|
+
if user_id not in usage_stats:
|
|
165
|
+
usage_stats[user_id] = {
|
|
166
|
+
"messages_sent": 0,
|
|
167
|
+
"tokens_used": 0,
|
|
168
|
+
"last_reset": "2025-01-01"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
plan = subscription_plans[current_plan]
|
|
172
|
+
messages_sent = usage_stats[user_id]["messages_sent"]
|
|
173
|
+
|
|
174
|
+
if plan["daily_limit"] and messages_sent >= plan["daily_limit"]:
|
|
175
|
+
return (False, f"Daily limit reached ({plan['daily_limit']} messages). Upgrade to Pro for more!")
|
|
176
|
+
|
|
177
|
+
return (True, "")
|
|
178
|
+
|
|
179
|
+
def increment_usage(user_id: str):
|
|
180
|
+
"""Increment usage counter"""
|
|
181
|
+
if user_id not in usage_stats:
|
|
182
|
+
usage_stats[user_id] = {
|
|
183
|
+
"messages_sent": 0,
|
|
184
|
+
"tokens_used": 0,
|
|
185
|
+
"last_reset": "2025-01-01"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
usage_stats[user_id]["messages_sent"] += 1
|
|
189
|
+
|
|
190
|
+
# ============================================
|
|
191
|
+
# Chat Endpoint
|
|
192
|
+
# ============================================
|
|
193
|
+
|
|
194
|
+
@app.post("/api/chat", response_model=ChatResponse)
|
|
195
|
+
async def chat_endpoint(request: ChatRequest):
|
|
196
|
+
try:
|
|
197
|
+
user_id = request.user_id or "default"
|
|
198
|
+
|
|
199
|
+
# Check usage limit
|
|
200
|
+
allowed, limit_message = check_limit(user_id)
|
|
201
|
+
if not allowed:
|
|
202
|
+
return ChatResponse(
|
|
203
|
+
success=False,
|
|
204
|
+
results=[],
|
|
205
|
+
cards=[],
|
|
206
|
+
visualization=None,
|
|
207
|
+
intent=None,
|
|
208
|
+
status="limit_reached",
|
|
209
|
+
limit_message=limit_message
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Increment usage counter
|
|
213
|
+
increment_usage(user_id)
|
|
214
|
+
|
|
215
|
+
# Run chat
|
|
216
|
+
result = run_chat(
|
|
217
|
+
message=request.message,
|
|
218
|
+
user_id=user_id,
|
|
219
|
+
conversation_history=request.conversation_history
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return ChatResponse(
|
|
223
|
+
success=True,
|
|
224
|
+
results=result.get("results", []),
|
|
225
|
+
cards=result.get("cards", []),
|
|
226
|
+
visualization=result.get("visualization"),
|
|
227
|
+
intent=result.get("intent"),
|
|
228
|
+
status=result.get("status", "success")
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Chat error: {e}")
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status_code=500,
|
|
235
|
+
detail="Internal server error"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@app.get("/api/test")
|
|
239
|
+
async def test_endpoint():
|
|
240
|
+
return {
|
|
241
|
+
"message": "Test endpoint working!",
|
|
242
|
+
"timestamp": "now"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@app.post("/api/personality/analyze")
|
|
246
|
+
async def analyze_personality(request: PersonalityAnalysisRequest):
|
|
247
|
+
try:
|
|
248
|
+
analysis = personality_analyzer.analyze_conversation(
|
|
249
|
+
user_id=request.user_id,
|
|
250
|
+
messages=request.messages
|
|
251
|
+
)
|
|
252
|
+
return {"success": True, "analysis": analysis}
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(f"Personality analysis error: {e}")
|
|
255
|
+
raise HTTPException(status_code=500, detail="Analysis failed")
|
|
256
|
+
|
|
257
|
+
@app.get("/api/personality/{user_id}")
|
|
258
|
+
async def get_personality(user_id: str):
|
|
259
|
+
analysis = personality_analyzer.get_analysis(user_id)
|
|
260
|
+
if analysis:
|
|
261
|
+
return {"success": True, "analysis": analysis}
|
|
262
|
+
return {"success": False, "message": "No analysis found"}
|
|
263
|
+
|
|
264
|
+
@app.post("/api/ai-chat/start")
|
|
265
|
+
async def start_ai_chat(request: MatchRequest):
|
|
266
|
+
try:
|
|
267
|
+
result = agent_system.start_ai_chat(
|
|
268
|
+
user1_id=request.user1_id,
|
|
269
|
+
user2_id=request.user2_id
|
|
270
|
+
)
|
|
271
|
+
return result
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(f"AI chat error: {e}")
|
|
274
|
+
raise HTTPException(status_code=500, detail="AI chat failed")
|
|
275
|
+
|
|
276
|
+
@app.post("/api/speech/transcribe")
|
|
277
|
+
async def transcribe_speech(file: UploadFile = File(...)):
|
|
278
|
+
try:
|
|
279
|
+
text = await speech_recognition.transcribe_audio(file)
|
|
280
|
+
if text:
|
|
281
|
+
return {"success": True, "text": text}
|
|
282
|
+
return {"success": False, "message": "Speech recognition not available"}
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print(f"Speech transcription error: {e}")
|
|
285
|
+
raise HTTPException(status_code=500, detail="Transcription failed")
|
|
286
|
+
|
|
287
|
+
@app.get("/api/tools/list")
|
|
288
|
+
async def list_tools():
|
|
289
|
+
tools = mcp_tool_manager.list_tools()
|
|
290
|
+
return {
|
|
291
|
+
"success": True,
|
|
292
|
+
"tools": [
|
|
293
|
+
{
|
|
294
|
+
"name": t.name,
|
|
295
|
+
"description": t.description,
|
|
296
|
+
"type": t.type.value,
|
|
297
|
+
"parameters": [
|
|
298
|
+
{"name": p.name, "type": p.type, "description": p.description, "required": p.required}
|
|
299
|
+
for p in t.parameters
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
for t in tools
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
class ToolExecuteRequest(BaseModel):
|
|
307
|
+
tool_name: str
|
|
308
|
+
params: Dict[str, Any]
|
|
309
|
+
|
|
310
|
+
class TitleGenerateRequest(BaseModel):
|
|
311
|
+
message: str
|
|
312
|
+
conversation_history: Optional[List[Dict[str, str]]] = None
|
|
313
|
+
|
|
314
|
+
@app.post("/api/tools/execute")
|
|
315
|
+
async def execute_tool(request: ToolExecuteRequest):
|
|
316
|
+
try:
|
|
317
|
+
result = await mcp_tool_manager.execute_tool(
|
|
318
|
+
tool_name=request.tool_name,
|
|
319
|
+
params=request.params
|
|
320
|
+
)
|
|
321
|
+
return result
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f"Tool execution error: {e}")
|
|
324
|
+
raise HTTPException(status_code=500, detail="Tool execution failed")
|
|
325
|
+
|
|
326
|
+
@app.post("/api/chat/title")
|
|
327
|
+
async def generate_chat_title(request: TitleGenerateRequest):
|
|
328
|
+
"""
|
|
329
|
+
Generate a session title using AI (Claude/Haiku model).
|
|
330
|
+
|
|
331
|
+
This is the single source of truth for AI-generated session titles.
|
|
332
|
+
Follows the same pattern as Claude Code's sessionTitle.ts
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
request: TitleGenerateRequest with user's first message
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
JSON with generated title or fallback
|
|
339
|
+
|
|
340
|
+
Example response:
|
|
341
|
+
{"success": true, "title": "Fix login button on mobile"}
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
# Check if API key is configured
|
|
345
|
+
if not GROK_API_KEY:
|
|
346
|
+
print("[Title API] No API key configured, using fallback")
|
|
347
|
+
return {
|
|
348
|
+
"success": False,
|
|
349
|
+
"title": generate_fallback_title(request.message),
|
|
350
|
+
"method": "fallback"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Call AI model to generate title
|
|
354
|
+
try:
|
|
355
|
+
from langchain_groq import ChatGroq
|
|
356
|
+
from langchain.prompts import ChatPromptTemplate
|
|
357
|
+
import json
|
|
358
|
+
|
|
359
|
+
# Use a lightweight model for title generation (like Haiku in Claude Code)
|
|
360
|
+
llm = ChatGroq(
|
|
361
|
+
api_key=GROK_API_KEY,
|
|
362
|
+
model="mixtral-8x7b-32768", # Fast and cheap
|
|
363
|
+
temperature=0.3 # Low temperature for consistency
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Professional prompt design (inspired by Claude Code)
|
|
367
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
368
|
+
("system", """Generate a concise, sentence-case title (3-7 words or 10-20 Chinese characters) that captures the main topic or goal of this conversation.
|
|
369
|
+
|
|
370
|
+
Requirements:
|
|
371
|
+
1. Be specific and descriptive
|
|
372
|
+
2. Use sentence case (capitalize only first word and proper nouns)
|
|
373
|
+
3. No quotes, colons, or punctuation in the title
|
|
374
|
+
4. Avoid vague titles like "Chat" or "Question"
|
|
375
|
+
5. Return ONLY the title text, no JSON, no explanations
|
|
376
|
+
|
|
377
|
+
Good examples:
|
|
378
|
+
- "Fix login button on mobile"
|
|
379
|
+
- "Add OAuth authentication"
|
|
380
|
+
- "推荐性价比高的笔记本电脑"
|
|
381
|
+
- "查询北京明天天气"
|
|
382
|
+
|
|
383
|
+
Bad examples:
|
|
384
|
+
- "Code changes" (too vague)
|
|
385
|
+
- "Investigate and fix the issue where..." (too long)
|
|
386
|
+
- "Fix Login Button On Mobile" (wrong case)
|
|
387
|
+
- "Chat about coding" (vague)"""),
|
|
388
|
+
("human", "Generate a title for this conversation:\n\n{message}")
|
|
389
|
+
])
|
|
390
|
+
|
|
391
|
+
chain = prompt | llm
|
|
392
|
+
response = chain.invoke({"message": request.message})
|
|
393
|
+
|
|
394
|
+
# Extract and clean title
|
|
395
|
+
title = response.content.strip()
|
|
396
|
+
|
|
397
|
+
# Remove quotes and common prefixes
|
|
398
|
+
title = title.strip('"\'`。.')
|
|
399
|
+
if title.lower().startswith('title:'):
|
|
400
|
+
title = title[6:].strip()
|
|
401
|
+
|
|
402
|
+
# Validate title
|
|
403
|
+
if not title or len(title) < 2:
|
|
404
|
+
raise ValueError("Title too short")
|
|
405
|
+
|
|
406
|
+
print(f"[Title API] Successfully generated: '{title}'")
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
"success": True,
|
|
410
|
+
"title": title,
|
|
411
|
+
"method": "ai"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
except Exception as ai_error:
|
|
415
|
+
print(f"[Title API] AI generation failed: {ai_error}")
|
|
416
|
+
# Fallback to rule-based generation
|
|
417
|
+
fallback_title = generate_fallback_title(request.message)
|
|
418
|
+
return {
|
|
419
|
+
"success": False,
|
|
420
|
+
"title": fallback_title,
|
|
421
|
+
"method": "fallback",
|
|
422
|
+
"error": str(ai_error)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
print(f"[Title API] Unexpected error: {e}")
|
|
427
|
+
return {
|
|
428
|
+
"success": False,
|
|
429
|
+
"title": generate_fallback_title(request.message),
|
|
430
|
+
"method": "fallback",
|
|
431
|
+
"error": str(e)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
def generate_fallback_title(message: str) -> str:
|
|
435
|
+
"""
|
|
436
|
+
备用标题生成方案(当 API 不可用时)
|
|
437
|
+
基于关键词匹配和文本提取
|
|
438
|
+
"""
|
|
439
|
+
if not message or message.strip() == '':
|
|
440
|
+
return 'New conversation'
|
|
441
|
+
|
|
442
|
+
message_lower = message.lower()
|
|
443
|
+
|
|
444
|
+
# 基于关键词的语义匹配
|
|
445
|
+
topic_keywords = {
|
|
446
|
+
'Shopping': ['buy', 'product', 'shop', 'recommend', 'price', 'cheap', 'expensive', 'purchase', 'amazon', '商品', '购物', '买', '推荐'],
|
|
447
|
+
'Food Order': ['food', 'eat', 'restaurant', 'order', 'delivery', 'hungry', 'meal', 'lunch', 'dinner', '饿了', '吃', '餐厅', '外卖'],
|
|
448
|
+
'Travel Planning': ['travel', 'flight', 'trip', 'hotel', 'vacation', 'book', 'ticket', 'airport', '旅行', '机票', '酒店'],
|
|
449
|
+
'Ride Booking': ['taxi', 'ride', 'uber', 'grab', 'car', 'drive', 'pickup', '打车', '叫车'],
|
|
450
|
+
'Weather': ['weather', 'temperature', 'rain', 'sunny', 'forecast', 'cold', 'hot', '天气', '下雨', '温度'],
|
|
451
|
+
'Schedule': ['meeting', 'schedule', 'calendar', 'event', 'appointment', 'reminder', '会议', '日程', '提醒', '安排'],
|
|
452
|
+
'Tech': ['computer', 'laptop', 'phone', 'tech', 'electronic', 'device', 'cpu', 'gpu', '电脑', '手机', '电子'],
|
|
453
|
+
'Fashion': ['clothes', 'shirt', 'dress', 'shoes', 'wear', 'fashion', 'style', '衣服', '时尚', '鞋子'],
|
|
454
|
+
'Health': ['health', 'exercise', 'gym', 'workout', 'diet', 'fitness', 'medicine', '健康', '运动', '健身'],
|
|
455
|
+
'Entertainment': ['movie', 'music', 'game', 'book', 'show', 'play', 'watch', '电影', '音乐', '游戏', '书']
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
# 检测主题
|
|
459
|
+
for topic, keywords in topic_keywords.items():
|
|
460
|
+
if any(keyword in message_lower for keyword in keywords):
|
|
461
|
+
return topic
|
|
462
|
+
|
|
463
|
+
# 提取第一个完整子句(到逗号或标点)
|
|
464
|
+
first_clause = message.split(r'[,.;:!?]')[0].strip()
|
|
465
|
+
if first_clause and len(first_clause) <= 30:
|
|
466
|
+
return first_clause
|
|
467
|
+
|
|
468
|
+
# 截取前 30 个字符
|
|
469
|
+
return message[:30] + ('...' if len(message) > 30 else '')
|
|
470
|
+
|
|
471
|
+
# ============================================
|
|
472
|
+
# Virtual Avatar System API Endpoints
|
|
473
|
+
# ============================================
|
|
474
|
+
|
|
475
|
+
class CreateAvatarRequest(BaseModel):
|
|
476
|
+
name: str
|
|
477
|
+
description: Optional[str] = ""
|
|
478
|
+
personality_traits: Optional[List[str]] = None
|
|
479
|
+
current_age: Optional[int] = 25
|
|
480
|
+
creator_id: Optional[str] = None
|
|
481
|
+
|
|
482
|
+
class UpdateAvatarRequest(BaseModel):
|
|
483
|
+
name: Optional[str] = None
|
|
484
|
+
description: Optional[str] = None
|
|
485
|
+
personality_traits: Optional[List[str]] = None
|
|
486
|
+
current_age: Optional[int] = None
|
|
487
|
+
avatar_image: Optional[str] = None
|
|
488
|
+
|
|
489
|
+
class SetAgeRequest(BaseModel):
|
|
490
|
+
age: int
|
|
491
|
+
|
|
492
|
+
class AddMemoryRequest(BaseModel):
|
|
493
|
+
content: str
|
|
494
|
+
min_age: Optional[int] = 0
|
|
495
|
+
max_age: Optional[int] = 150
|
|
496
|
+
clarity: Optional[int] = 80
|
|
497
|
+
emotional_tone: Optional[str] = "neutral"
|
|
498
|
+
topic: Optional[str] = None
|
|
499
|
+
|
|
500
|
+
class ExportAvatarRequest(BaseModel):
|
|
501
|
+
export_format: Optional[str] = "avatar"
|
|
502
|
+
|
|
503
|
+
class ListForSaleRequest(BaseModel):
|
|
504
|
+
price: float
|
|
505
|
+
description: Optional[str] = None
|
|
506
|
+
|
|
507
|
+
class PurchaseAvatarRequest(BaseModel):
|
|
508
|
+
buyer_id: str
|
|
509
|
+
|
|
510
|
+
@app.get("/api/avatar")
|
|
511
|
+
async def list_avatars(creator_id: Optional[str] = None):
|
|
512
|
+
"""List all avatars, optionally filtered by creator"""
|
|
513
|
+
avatars = avatar_manager.list_avatars(creator_id)
|
|
514
|
+
return {"success": True, "avatars": avatars}
|
|
515
|
+
|
|
516
|
+
@app.post("/api/avatar")
|
|
517
|
+
async def create_avatar(request: CreateAvatarRequest):
|
|
518
|
+
"""Create a new virtual avatar"""
|
|
519
|
+
try:
|
|
520
|
+
avatar = avatar_manager.create_avatar(
|
|
521
|
+
name=request.name,
|
|
522
|
+
description=request.description,
|
|
523
|
+
personality_traits=request.personality_traits,
|
|
524
|
+
current_age=request.current_age,
|
|
525
|
+
creator_id=request.creator_id
|
|
526
|
+
)
|
|
527
|
+
return {"success": True, "avatar": avatar.to_dict()}
|
|
528
|
+
except Exception as e:
|
|
529
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
530
|
+
|
|
531
|
+
@app.get("/api/avatar/{avatar_id}")
|
|
532
|
+
async def get_avatar(avatar_id: str):
|
|
533
|
+
"""Get a specific avatar by ID"""
|
|
534
|
+
avatar = avatar_manager.load_avatar(avatar_id)
|
|
535
|
+
if avatar:
|
|
536
|
+
return {"success": True, "avatar": avatar.to_dict()}
|
|
537
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
538
|
+
|
|
539
|
+
@app.put("/api/avatar/{avatar_id}")
|
|
540
|
+
async def update_avatar(avatar_id: str, request: UpdateAvatarRequest):
|
|
541
|
+
"""Update avatar information"""
|
|
542
|
+
update_data = {k: v for k, v in request.dict().items() if v is not None}
|
|
543
|
+
avatar = avatar_manager.update_avatar(avatar_id, **update_data)
|
|
544
|
+
if avatar:
|
|
545
|
+
return {"success": True, "avatar": avatar.to_dict()}
|
|
546
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
547
|
+
|
|
548
|
+
@app.delete("/api/avatar/{avatar_id}")
|
|
549
|
+
async def delete_avatar(avatar_id: str):
|
|
550
|
+
"""Delete an avatar"""
|
|
551
|
+
deleted = avatar_manager.delete_avatar(avatar_id)
|
|
552
|
+
if deleted:
|
|
553
|
+
return {"success": True, "message": "Avatar deleted"}
|
|
554
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
555
|
+
|
|
556
|
+
@app.post("/api/avatar/{avatar_id}/age")
|
|
557
|
+
async def set_avatar_age(avatar_id: str, request: SetAgeRequest):
|
|
558
|
+
"""Set avatar's current age"""
|
|
559
|
+
avatar = avatar_manager.set_age(avatar_id, request.age)
|
|
560
|
+
if avatar:
|
|
561
|
+
return {"success": True, "avatar": avatar.to_dict()}
|
|
562
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
563
|
+
|
|
564
|
+
@app.get("/api/avatar/{avatar_id}/memories")
|
|
565
|
+
async def get_avatar_memories(avatar_id: str, age: Optional[int] = None):
|
|
566
|
+
"""Get avatar memories, optionally filtered by age"""
|
|
567
|
+
if age is not None:
|
|
568
|
+
memories = avatar_manager.get_memories_for_age(avatar_id, age)
|
|
569
|
+
else:
|
|
570
|
+
memories = tree_memory_system.get_all_memories(avatar_id)
|
|
571
|
+
return {"success": True, "memories": memories, "filter_age": age}
|
|
572
|
+
|
|
573
|
+
@app.post("/api/avatar/{avatar_id}/memories")
|
|
574
|
+
async def add_memory(avatar_id: str, request: AddMemoryRequest):
|
|
575
|
+
"""Add a new memory to avatar"""
|
|
576
|
+
try:
|
|
577
|
+
memory_id = avatar_manager.add_memory(
|
|
578
|
+
avatar_id=avatar_id,
|
|
579
|
+
content=request.content,
|
|
580
|
+
min_age=request.min_age,
|
|
581
|
+
max_age=request.max_age,
|
|
582
|
+
clarity=request.clarity,
|
|
583
|
+
emotional_tone=request.emotional_tone,
|
|
584
|
+
topic=request.topic
|
|
585
|
+
)
|
|
586
|
+
return {"success": True, "memory_id": memory_id}
|
|
587
|
+
except Exception as e:
|
|
588
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
589
|
+
|
|
590
|
+
@app.delete("/api/avatar/{avatar_id}/memories/{memory_id}")
|
|
591
|
+
async def delete_memory(avatar_id: str, memory_id: str):
|
|
592
|
+
"""Delete a memory"""
|
|
593
|
+
deleted = tree_memory_system.delete_memory(avatar_id, memory_id)
|
|
594
|
+
if deleted:
|
|
595
|
+
return {"success": True, "message": "Memory deleted"}
|
|
596
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
597
|
+
|
|
598
|
+
@app.get("/api/avatar/{avatar_id}/tree")
|
|
599
|
+
async def get_memory_tree(avatar_id: str):
|
|
600
|
+
"""Get the inverted tree memory structure"""
|
|
601
|
+
tree = avatar_manager.get_tree_structure(avatar_id)
|
|
602
|
+
if tree:
|
|
603
|
+
return {"success": True, "tree": tree}
|
|
604
|
+
raise HTTPException(status_code=404, detail="Tree not found")
|
|
605
|
+
|
|
606
|
+
class TreeMemorySearchRequest(BaseModel):
|
|
607
|
+
query: str
|
|
608
|
+
min_relevance: Optional[float] = 0.3
|
|
609
|
+
max_results: Optional[int] = 10
|
|
610
|
+
|
|
611
|
+
@app.post("/api/avatar/{avatar_id}/memories/search")
|
|
612
|
+
async def search_tree_memories(avatar_id: str, request: TreeMemorySearchRequest):
|
|
613
|
+
"""Search tree memories by relevance"""
|
|
614
|
+
memories = tree_memory_system.search_relevant_memories(
|
|
615
|
+
avatar_id,
|
|
616
|
+
request.query,
|
|
617
|
+
request.min_relevance,
|
|
618
|
+
request.max_results
|
|
619
|
+
)
|
|
620
|
+
return {"success": True, "memories": memories}
|
|
621
|
+
|
|
622
|
+
@app.get("/api/avatar/{avatar_id}/memories/context")
|
|
623
|
+
async def get_tree_context(avatar_id: str, query: str, min_relevance: Optional[float] = 0.3, max_results: Optional[int] = 5):
|
|
624
|
+
"""Get formatted context from tree memories"""
|
|
625
|
+
context = tree_memory_system.get_relevant_context(
|
|
626
|
+
avatar_id,
|
|
627
|
+
query,
|
|
628
|
+
min_relevance,
|
|
629
|
+
max_results
|
|
630
|
+
)
|
|
631
|
+
return {"success": True, "context": context}
|
|
632
|
+
|
|
633
|
+
@app.post("/api/avatar/{avatar_id}/developer-mode")
|
|
634
|
+
async def toggle_developer_mode(avatar_id: str):
|
|
635
|
+
"""Toggle developer mode for avatar"""
|
|
636
|
+
enabled = avatar_manager.toggle_developer_mode(avatar_id)
|
|
637
|
+
if enabled is not None:
|
|
638
|
+
return {"success": True, "developer_mode": enabled}
|
|
639
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
640
|
+
|
|
641
|
+
@app.post("/api/avatar/{avatar_id}/export")
|
|
642
|
+
async def export_avatar(avatar_id: str, request: ExportAvatarRequest):
|
|
643
|
+
"""Export avatar to file"""
|
|
644
|
+
filepath = avatar_packer.export_avatar(avatar_id, request.export_format)
|
|
645
|
+
if filepath:
|
|
646
|
+
return {"success": True, "filepath": filepath, "filename": os.path.basename(filepath)}
|
|
647
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
648
|
+
|
|
649
|
+
@app.get("/api/exports")
|
|
650
|
+
async def list_exports():
|
|
651
|
+
"""List all exported files"""
|
|
652
|
+
exports = avatar_packer.list_exports()
|
|
653
|
+
return {"success": True, "exports": exports}
|
|
654
|
+
|
|
655
|
+
@app.delete("/api/exports/{filename}")
|
|
656
|
+
async def delete_export(filename: str):
|
|
657
|
+
"""Delete an exported file"""
|
|
658
|
+
deleted = avatar_packer.delete_export(filename)
|
|
659
|
+
if deleted:
|
|
660
|
+
return {"success": True, "message": "Export deleted"}
|
|
661
|
+
raise HTTPException(status_code=404, detail="Export not found")
|
|
662
|
+
|
|
663
|
+
@app.post("/api/avatar/{avatar_id}/list-for-sale")
|
|
664
|
+
async def list_for_sale(avatar_id: str, request: ListForSaleRequest):
|
|
665
|
+
"""List avatar for sale on marketplace"""
|
|
666
|
+
success = marketplace.list_for_sale(avatar_id, request.price, request.description)
|
|
667
|
+
if success:
|
|
668
|
+
return {"success": True, "message": "Avatar listed for sale"}
|
|
669
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
670
|
+
|
|
671
|
+
@app.post("/api/avatar/{avatar_id}/remove-from-sale")
|
|
672
|
+
async def remove_from_sale(avatar_id: str):
|
|
673
|
+
"""Remove avatar from marketplace"""
|
|
674
|
+
success = marketplace.remove_from_sale(avatar_id)
|
|
675
|
+
if success:
|
|
676
|
+
return {"success": True, "message": "Avatar removed from sale"}
|
|
677
|
+
raise HTTPException(status_code=404, detail="Avatar not found")
|
|
678
|
+
|
|
679
|
+
@app.get("/api/marketplace")
|
|
680
|
+
async def get_marketplace():
|
|
681
|
+
"""Get all avatars listed for sale"""
|
|
682
|
+
listings = marketplace.get_market_listings()
|
|
683
|
+
return {"success": True, "listings": listings}
|
|
684
|
+
|
|
685
|
+
@app.post("/api/marketplace/{avatar_id}/purchase")
|
|
686
|
+
async def purchase_avatar(avatar_id: str, request: PurchaseAvatarRequest):
|
|
687
|
+
"""Purchase an avatar from marketplace"""
|
|
688
|
+
new_avatar_id = marketplace.purchase_avatar(avatar_id, request.buyer_id)
|
|
689
|
+
if new_avatar_id:
|
|
690
|
+
return {"success": True, "new_avatar_id": new_avatar_id, "message": "Avatar purchased successfully"}
|
|
691
|
+
raise HTTPException(status_code=404, detail="Avatar not available for purchase")
|
|
692
|
+
|
|
693
|
+
class SelectAvatarRequest(BaseModel):
|
|
694
|
+
user_id: str
|
|
695
|
+
avatar_id: str
|
|
696
|
+
|
|
697
|
+
@app.post("/api/user/select-avatar")
|
|
698
|
+
async def select_user_avatar(request: SelectAvatarRequest):
|
|
699
|
+
"""Select an avatar for the user"""
|
|
700
|
+
try:
|
|
701
|
+
user_settings_file = os.path.join("user_data", f"{request.user_id}_settings.json")
|
|
702
|
+
os.makedirs("user_data", exist_ok=True)
|
|
703
|
+
|
|
704
|
+
settings = {}
|
|
705
|
+
if os.path.exists(user_settings_file):
|
|
706
|
+
with open(user_settings_file, "r", encoding="utf-8") as f:
|
|
707
|
+
settings = json.load(f)
|
|
708
|
+
|
|
709
|
+
settings["selected_avatar_id"] = request.avatar_id
|
|
710
|
+
settings["selected_at"] = datetime.now().isoformat()
|
|
711
|
+
|
|
712
|
+
with open(user_settings_file, "w", encoding="utf-8") as f:
|
|
713
|
+
json.dump(settings, f, ensure_ascii=False, indent=2)
|
|
714
|
+
|
|
715
|
+
return {"success": True, "selected_avatar_id": request.avatar_id}
|
|
716
|
+
except Exception as e:
|
|
717
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
718
|
+
|
|
719
|
+
@app.get("/api/user/selected-avatar")
|
|
720
|
+
async def get_selected_avatar(user_id: str):
|
|
721
|
+
"""Get user's selected avatar"""
|
|
722
|
+
try:
|
|
723
|
+
user_settings_file = os.path.join("user_data", f"{user_id}_settings.json")
|
|
724
|
+
|
|
725
|
+
if os.path.exists(user_settings_file):
|
|
726
|
+
with open(user_settings_file, "r", encoding="utf-8") as f:
|
|
727
|
+
settings = json.load(f)
|
|
728
|
+
avatar_id = settings.get("selected_avatar_id")
|
|
729
|
+
if avatar_id:
|
|
730
|
+
avatar = avatar_manager.load_avatar(avatar_id)
|
|
731
|
+
if avatar:
|
|
732
|
+
return {"success": True, "avatar": avatar.to_dict()}
|
|
733
|
+
|
|
734
|
+
return {"success": True, "avatar": None}
|
|
735
|
+
except Exception as e:
|
|
736
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
737
|
+
|
|
738
|
+
@app.get("/api/user/my-avatars")
|
|
739
|
+
async def get_user_avatars(user_id: str):
|
|
740
|
+
"""Get all avatars owned by user"""
|
|
741
|
+
try:
|
|
742
|
+
avatars = avatar_manager.list_avatars(user_id)
|
|
743
|
+
return {"success": True, "avatars": avatars}
|
|
744
|
+
except Exception as e:
|
|
745
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
746
|
+
|
|
747
|
+
@app.get("/avatar-manager")
|
|
748
|
+
async def avatar_manager():
|
|
749
|
+
"""Virtual Avatar Manager Page"""
|
|
750
|
+
manager_path = os.path.join(os.path.dirname(__file__), "static", "avatar-manager.html")
|
|
751
|
+
if os.path.exists(manager_path):
|
|
752
|
+
return FileResponse(manager_path)
|
|
753
|
+
raise HTTPException(status_code=404, detail="Page not found")
|
|
754
|
+
|
|
755
|
+
@app.get("/design-template")
|
|
756
|
+
async def design_template():
|
|
757
|
+
"""Design Template Preview Page"""
|
|
758
|
+
template_path = os.path.join(os.path.dirname(__file__), "static", "design-template.html")
|
|
759
|
+
if os.path.exists(template_path):
|
|
760
|
+
return FileResponse(template_path)
|
|
761
|
+
raise HTTPException(status_code=404, detail="Design template page not found")
|
|
762
|
+
|
|
763
|
+
@app.get("/design-template-roblox")
|
|
764
|
+
async def design_template_roblox():
|
|
765
|
+
"""Roblox Style Design Template Preview Page"""
|
|
766
|
+
template_path = os.path.join(os.path.dirname(__file__), "static", "design-template-roblox.html")
|
|
767
|
+
if os.path.exists(template_path):
|
|
768
|
+
return FileResponse(template_path)
|
|
769
|
+
raise HTTPException(status_code=404, detail="Roblox design template page not found")
|
|
770
|
+
|
|
771
|
+
@app.get("/roblox-interface")
|
|
772
|
+
async def roblox_interface():
|
|
773
|
+
"""Full Roblox Style Interface"""
|
|
774
|
+
interface_path = os.path.join(os.path.dirname(__file__), "static", "roblox-interface.html")
|
|
775
|
+
if os.path.exists(interface_path):
|
|
776
|
+
return FileResponse(interface_path)
|
|
777
|
+
raise HTTPException(status_code=404, detail="Roblox interface page not found")
|
|
778
|
+
|
|
779
|
+
@app.get("/grok-roblox-combo")
|
|
780
|
+
async def grok_roblox_combo():
|
|
781
|
+
"""Grok Layout + Roblox Style Interface"""
|
|
782
|
+
interface_path = os.path.join(os.path.dirname(__file__), "static", "grok-roblox-combo.html")
|
|
783
|
+
if os.path.exists(interface_path):
|
|
784
|
+
return FileResponse(interface_path)
|
|
785
|
+
raise HTTPException(status_code=404, detail="Grok-Roblox combo page not found")
|
|
786
|
+
|
|
787
|
+
@app.get("/template-main")
|
|
788
|
+
async def template_main():
|
|
789
|
+
"""Template - Main Chat Interface"""
|
|
790
|
+
template_path = os.path.join(os.path.dirname(__file__), "static", "template-main.html")
|
|
791
|
+
if os.path.exists(template_path):
|
|
792
|
+
return FileResponse(template_path)
|
|
793
|
+
raise HTTPException(status_code=404, detail="Template main page not found")
|
|
794
|
+
|
|
795
|
+
@app.get("/template-profile")
|
|
796
|
+
async def template_profile():
|
|
797
|
+
"""Template - Profile Settings Page"""
|
|
798
|
+
template_path = os.path.join(os.path.dirname(__file__), "static", "template-profile.html")
|
|
799
|
+
if os.path.exists(template_path):
|
|
800
|
+
return FileResponse(template_path)
|
|
801
|
+
raise HTTPException(status_code=404, detail="Template profile page not found")
|
|
802
|
+
|
|
803
|
+
@app.get("/template-roblox")
|
|
804
|
+
async def template_roblox():
|
|
805
|
+
"""Template - Roblox Avatar Manager"""
|
|
806
|
+
template_path = os.path.join(os.path.dirname(__file__), "static", "template-roblox.html")
|
|
807
|
+
if os.path.exists(template_path):
|
|
808
|
+
return FileResponse(template_path)
|
|
809
|
+
raise HTTPException(status_code=404, detail="Template Roblox page not found")
|
|
810
|
+
|
|
811
|
+
@app.get("/avatar-manager-real")
|
|
812
|
+
async def avatar_manager_real():
|
|
813
|
+
"""Real Avatar Manager with Import/Export"""
|
|
814
|
+
manager_path = os.path.join(os.path.dirname(__file__), "static", "avatar-manager-real.html")
|
|
815
|
+
if os.path.exists(manager_path):
|
|
816
|
+
return FileResponse(manager_path)
|
|
817
|
+
raise HTTPException(status_code=404, detail="Avatar manager page not found")
|
|
818
|
+
|
|
819
|
+
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
|
820
|
+
if os.path.exists(static_dir):
|
|
821
|
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
822
|
+
|
|
823
|
+
if __name__ == "__main__":
|
|
824
|
+
import uvicorn
|
|
825
|
+
uvicorn.run(
|
|
826
|
+
"main:app",
|
|
827
|
+
host="0.0.0.0",
|
|
828
|
+
port=8000,
|
|
829
|
+
reload=DEBUG
|
|
830
|
+
)
|