Cuong2004 commited on
Commit
45b1ef5
·
1 Parent(s): 9882d96

gg auth and new tool

Browse files
.env.example CHANGED
@@ -16,9 +16,19 @@ NEO4J_PASSWORD=CHANGE_ME
16
  # Google AI (Gemini)
17
  GOOGLE_API_KEY=your_google_api_key
18
 
 
 
 
 
19
  # MegaLLM (OpenAI-compatible - DeepSeek)
20
  MEGALLM_API_KEY=your_megallm_api_key
21
  MEGALLM_BASE_URL=https://ai.megallm.io/v1
22
 
 
 
 
 
 
 
23
  # CLIP (optional - for image embeddings)
24
  HUGGINGFACE_API_KEY=your_hf_api_key
 
16
  # Google AI (Gemini)
17
  GOOGLE_API_KEY=your_google_api_key
18
 
19
+ # Google OAuth
20
+ GOOGLE_CLIENT_ID=your_google_api_key
21
+ JWT_SECRET=your-super-secret-jwt-key-change-in-production
22
+
23
  # MegaLLM (OpenAI-compatible - DeepSeek)
24
  MEGALLM_API_KEY=your_megallm_api_key
25
  MEGALLM_BASE_URL=https://ai.megallm.io/v1
26
 
27
+ # Brave Social Search
28
+ BRAVE_API_KEY=your_brave_api_key
29
+
30
+ # Google OAuth
31
+ GOOGLE_CLIENT_ID=your_google_client_id
32
+
33
  # CLIP (optional - for image embeddings)
34
  HUGGINGFACE_API_KEY=your_hf_api_key
app/agent/mmca_agent.py CHANGED
@@ -43,16 +43,22 @@ SYSTEM_PROMPT = """Bạn là trợ lý du lịch thông minh cho Đà Nẵng. B
43
  - Ví dụ: "Quán cafe gần Cầu Rồng", "Nhà hàng gần bãi biển Mỹ Khê"
44
  - Đặc biệt: Có thể lấy chi tiết đầy đủ với photos, reviews
45
 
 
 
 
 
 
46
  **Quy tắc quan trọng:**
47
  1. Phân tích intent để chọn ĐÚNG tool (không chỉ dùng 1 tool)
48
  2. Với câu hỏi tổng quát ("quán cafe ngon") → dùng retrieve_context_text
49
  3. Với câu hỏi vị trí ("gần X", "quanh Y") → dùng find_nearby_places
50
- 4. Với ảnh dùng retrieve_similar_visuals
51
- 5. thể kết hợp nhiều tools để có kết quả tốt nhất
52
  6. Trả lời tiếng Việt, thân thiện, cung cấp thông tin cụ thể (tên, rating, khoảng cách)
53
  """
54
 
55
 
 
56
  @dataclass
57
  class ChatMessage:
58
  """Chat message model."""
@@ -244,6 +250,11 @@ class MMCAAgent:
244
  if not intents:
245
  intents.append("text_search")
246
 
 
 
 
 
 
247
  return " + ".join(intents)
248
 
249
  def _get_tool_purpose(self, tool_name: str) -> str:
@@ -252,6 +263,7 @@ class MMCAAgent:
252
  "retrieve_context_text": "Tìm kiếm semantic trong văn bản (review, mô tả)",
253
  "retrieve_similar_visuals": "Tìm địa điểm có hình ảnh tương tự",
254
  "find_nearby_places": "Tìm địa điểm gần vị trí được nhắc đến",
 
255
  }
256
  return purposes.get(tool_name, tool_name)
257
 
@@ -274,6 +286,30 @@ class MMCAAgent:
274
  arguments={"image_url": image_url, "limit": 5},
275
  ))
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  # Analyze message for location/proximity queries
278
  location_keywords = ["gần", "cách", "nearby", "gần đây", "quanh", "xung quanh"]
279
  if any(kw in message.lower() for kw in location_keywords):
@@ -296,13 +332,16 @@ class MMCAAgent:
296
  },
297
  ))
298
 
299
- # For general queries without location keywords, use text search
 
 
300
  if not tool_calls:
301
  tool_calls.append(ToolCall(
302
  tool_name="retrieve_context_text",
303
  arguments={"query": message, "limit": 5},
304
  ))
305
 
 
306
  return tool_calls
307
 
308
  async def _execute_tool(
@@ -370,6 +409,25 @@ class MMCAAgent:
370
  for r in results
371
  ]
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  except Exception as e:
374
  agent_logger.error(f"Tool execution failed: {tool_call.tool_name}", e)
375
  tool_call.result = [{"error": str(e)}]
 
43
  - Ví dụ: "Quán cafe gần Cầu Rồng", "Nhà hàng gần bãi biển Mỹ Khê"
44
  - Đặc biệt: Có thể lấy chi tiết đầy đủ với photos, reviews
45
 
46
+ **4. search_social_media** - Tìm kiếm mạng xã hội và tin tức
47
+ - Khi nào: Hỏi về "review", "tin hot", "trend", "tiktok", "facebook", "tin mới"
48
+ - Ví dụ: "Review quán ăn ngon Đà Nẵng trên TikTok", "Tin hot tuần qua"
49
+ - Tham số: query, freshness ("pw": tuần, "pm": tháng), platforms (["tiktok", "facebook", "reddit"])
50
+
51
  **Quy tắc quan trọng:**
52
  1. Phân tích intent để chọn ĐÚNG tool (không chỉ dùng 1 tool)
53
  2. Với câu hỏi tổng quát ("quán cafe ngon") → dùng retrieve_context_text
54
  3. Với câu hỏi vị trí ("gần X", "quanh Y") → dùng find_nearby_places
55
+ 4. Với câu hỏi trend/review từ MXH -> dùng search_social_media
56
+ 5. Với ảnh dùng retrieve_similar_visuals
57
  6. Trả lời tiếng Việt, thân thiện, cung cấp thông tin cụ thể (tên, rating, khoảng cách)
58
  """
59
 
60
 
61
+
62
  @dataclass
63
  class ChatMessage:
64
  """Chat message model."""
 
250
  if not intents:
251
  intents.append("text_search")
252
 
253
+ # Social intent detection
254
+ social_keywords = ["review", "tin hot", "trend", "tin mới", "tiktok", "facebook", "reddit", "youtube", "mạng xã hội"]
255
+ if any(kw in message.lower() for kw in social_keywords):
256
+ intents.append("social_search")
257
+
258
  return " + ".join(intents)
259
 
260
  def _get_tool_purpose(self, tool_name: str) -> str:
 
263
  "retrieve_context_text": "Tìm kiếm semantic trong văn bản (review, mô tả)",
264
  "retrieve_similar_visuals": "Tìm địa điểm có hình ảnh tương tự",
265
  "find_nearby_places": "Tìm địa điểm gần vị trí được nhắc đến",
266
+ "search_social_media": "Tìm kiếm thông tin từ mạng xã hội (news, trends)",
267
  }
268
  return purposes.get(tool_name, tool_name)
269
 
 
286
  arguments={"image_url": image_url, "limit": 5},
287
  ))
288
 
289
+ # Check for social media intent FIRST
290
+ social_keywords = ["review", "tin hot", "trend", "tin mới", "tiktok", "facebook", "reddit", "youtube", "mạng xã hội"]
291
+ if any(kw in message.lower() for kw in social_keywords):
292
+ # Determine freshness
293
+ freshness = "pw" # Default past week
294
+ if "tháng" in message.lower() or "month" in message.lower():
295
+ freshness = "pm"
296
+
297
+ # Determine platforms
298
+ platforms = []
299
+ for p in ["tiktok", "facebook", "reddit", "youtube", "twitter", "instagram"]:
300
+ if p in message.lower():
301
+ platforms.append(p)
302
+
303
+ tool_calls.append(ToolCall(
304
+ tool_name="search_social_media",
305
+ arguments={
306
+ "query": message,
307
+ "limit": 5,
308
+ "freshness": freshness,
309
+ "platforms": platforms if platforms else None
310
+ }
311
+ ))
312
+
313
  # Analyze message for location/proximity queries
314
  location_keywords = ["gần", "cách", "nearby", "gần đây", "quanh", "xung quanh"]
315
  if any(kw in message.lower() for kw in location_keywords):
 
332
  },
333
  ))
334
 
335
+ # For general queries without location keywords AND NO SOCIAL INTENT, use text search
336
+ # If social search is already triggered, we might skip text search to avoid noise,
337
+ # or keep it if query is mixed. For now, let's keep text search only if no other tools used.
338
  if not tool_calls:
339
  tool_calls.append(ToolCall(
340
  tool_name="retrieve_context_text",
341
  arguments={"query": message, "limit": 5},
342
  ))
343
 
344
+
345
  return tool_calls
346
 
347
  async def _execute_tool(
 
409
  for r in results
410
  ]
411
 
412
+ elif tool_call.tool_name == "search_social_media":
413
+ results = await self.tools.search_social_media(
414
+ query=tool_call.arguments.get("query", ""),
415
+ limit=tool_call.arguments.get("limit", 10),
416
+ freshness=tool_call.arguments.get("freshness", "pw"),
417
+ platforms=tool_call.arguments.get("platforms"),
418
+ )
419
+ tool_call.result = [
420
+ {
421
+ "title": r.title,
422
+ "url": r.url,
423
+ "description": r.description,
424
+ "age": r.age,
425
+ "platform": r.platform,
426
+ }
427
+ for r in results
428
+ ]
429
+
430
+
431
  except Exception as e:
432
  agent_logger.error(f"Tool execution failed: {tool_call.tool_name}", e)
433
  tool_call.result = [{"error": str(e)}]
app/agent/react_agent.py CHANGED
@@ -259,16 +259,27 @@ class ReActAgent:
259
  db=db,
260
  image_url=url,
261
  limit=action_input.get("limit", 5),
 
 
 
 
 
 
 
 
 
 
262
  )
263
  return [
264
  {
265
- "place_id": r.place_id,
266
- "name": r.name,
267
- "category": r.category,
268
- "similarity": r.similarity,
269
  }
270
  for r in results
271
  ]
 
272
 
273
  else:
274
  return {"error": f"Unknown tool: {action}"}
 
259
  db=db,
260
  image_url=url,
261
  limit=action_input.get("limit", 5),
262
+ )
263
+ for r in results
264
+ ]
265
+
266
+ elif action == "search_social_media":
267
+ results = await self.tools.search_social_media(
268
+ query=action_input.get("query", ""),
269
+ limit=action_input.get("limit", 5),
270
+ freshness=action_input.get("freshness", "pw"),
271
+ platforms=action_input.get("platforms"),
272
  )
273
  return [
274
  {
275
+ "title": r.title,
276
+ "url": r.url,
277
+ "age": r.age,
278
+ "platform": r.platform,
279
  }
280
  for r in results
281
  ]
282
+
283
 
284
  else:
285
  return {"error": f"Unknown tool: {action}"}
app/agent/reasoning.py CHANGED
@@ -28,6 +28,10 @@ REACT_SYSTEM_PROMPT = """Bạn là agent du lịch thông minh cho Đà Nẵng v
28
  - Input: {"image_url": "..."}
29
  - Output: [{name, similarity, image_url}]
30
 
 
 
 
 
31
  **Quy trình:**
32
  Với mỗi bước, bạn phải:
33
  1. **Thought**: Suy nghĩ về bước tiếp theo cần làm
@@ -46,11 +50,13 @@ Với mỗi bước, bạn phải:
46
  **Quan trọng:**
47
  - Nếu cần biết vị trí → dùng get_location_coordinates trước
48
  - Nếu tìm theo khoảng cách → dùng find_nearby_places
 
49
  - Nếu cần lọc theo đặc điểm (view, không gian, giá) → dùng retrieve_context_text
50
  - Khi đủ thông tin → action = "finish"
51
  """
52
 
53
 
 
54
  @dataclass
55
  class ReasoningResult:
56
  """Result from LLM reasoning step."""
@@ -157,6 +163,8 @@ def get_tool_purpose(action: str) -> str:
157
  "find_nearby_places": "Tìm địa điểm gần vị trí",
158
  "retrieve_context_text": "Tìm theo văn bản (reviews, mô tả)",
159
  "retrieve_similar_visuals": "Tìm theo hình ảnh tương tự",
 
160
  "finish": "Hoàn thành và tổng hợp kết quả",
161
  }
 
162
  return purposes.get(action, action)
 
28
  - Input: {"image_url": "..."}
29
  - Output: [{name, similarity, image_url}]
30
 
31
+ 5. `search_social_media` - Tìm kiếm mạng xã hội và tin tức
32
+ - Input: {"query": "review quán ăn", "freshness": "pw", "platforms": ["tiktok"]}
33
+ - Output: [{title, url, age, platform}]
34
+
35
  **Quy trình:**
36
  Với mỗi bước, bạn phải:
37
  1. **Thought**: Suy nghĩ về bước tiếp theo cần làm
 
50
  **Quan trọng:**
51
  - Nếu cần biết vị trí → dùng get_location_coordinates trước
52
  - Nếu tìm theo khoảng cách → dùng find_nearby_places
53
+ - Nếu tìm review/trend MXH → dùng search_social_media
54
  - Nếu cần lọc theo đặc điểm (view, không gian, giá) → dùng retrieve_context_text
55
  - Khi đủ thông tin → action = "finish"
56
  """
57
 
58
 
59
+
60
  @dataclass
61
  class ReasoningResult:
62
  """Result from LLM reasoning step."""
 
163
  "find_nearby_places": "Tìm địa điểm gần vị trí",
164
  "retrieve_context_text": "Tìm theo văn bản (reviews, mô tả)",
165
  "retrieve_similar_visuals": "Tìm theo hình ảnh tương tự",
166
+ "search_social_media": "Tìm kiếm mạng xã hội và tin tức",
167
  "finish": "Hoàn thành và tổng hợp kết quả",
168
  }
169
+
170
  return purposes.get(action, action)
app/auth/controls.py CHANGED
@@ -8,12 +8,12 @@ from datetime import datetime, timedelta
8
  import jwt
9
  import os
10
  from uuid import uuid4
 
11
 
12
  # Google OAuth verification URL
13
- GOOGLE_VERIFY_URL = "https://www.googleapis.com/oauth2/v3/userinfo?access_token="
14
 
15
- # JWT settings (should be in environment variables)
16
- JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key-change-in-production")
17
  JWT_ALGORITHM = "HS256"
18
  JWT_EXPIRATION_HOURS = 24
19
 
@@ -43,7 +43,11 @@ async def login_control(access_token: str, db: AsyncSession) -> dict:
43
  # Verify token with Google
44
  async with httpx.AsyncClient() as client:
45
  try:
46
- response = await client.get(f"{GOOGLE_VERIFY_URL}{access_token}")
 
 
 
 
47
 
48
  if response.status_code != 200:
49
  raise HTTPException(
@@ -53,6 +57,10 @@ async def login_control(access_token: str, db: AsyncSession) -> dict:
53
 
54
  google_user_info = response.json()
55
 
 
 
 
 
56
  except httpx.RequestError as e:
57
  raise HTTPException(
58
  status_code=500,
@@ -133,7 +141,7 @@ async def login_control(access_token: str, db: AsyncSession) -> dict:
133
  "email": email,
134
  "exp": datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS)
135
  }
136
- token = jwt.encode(token_payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
137
 
138
  return {
139
  "user_id": user_id,
 
8
  import jwt
9
  import os
10
  from uuid import uuid4
11
+ from app.core.config import settings
12
 
13
  # Google OAuth verification URL
14
+ GOOGLE_VERIFY_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
15
 
16
+ # JWT settings
 
17
  JWT_ALGORITHM = "HS256"
18
  JWT_EXPIRATION_HOURS = 24
19
 
 
43
  # Verify token with Google
44
  async with httpx.AsyncClient() as client:
45
  try:
46
+ # Get user info using access token
47
+ response = await client.get(
48
+ GOOGLE_VERIFY_URL,
49
+ headers={"Authorization": f"Bearer {access_token}"}
50
+ )
51
 
52
  if response.status_code != 200:
53
  raise HTTPException(
 
57
 
58
  google_user_info = response.json()
59
 
60
+ # Verify the token was issued for our client
61
+ # Note: For access tokens from Token Client, we trust Google's validation
62
+ # The token is already validated by Google if we get a 200 response
63
+
64
  except httpx.RequestError as e:
65
  raise HTTPException(
66
  status_code=500,
 
141
  "email": email,
142
  "exp": datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS)
143
  }
144
+ token = jwt.encode(token_payload, settings.jwt_secret, algorithm=JWT_ALGORITHM)
145
 
146
  return {
147
  "user_id": user_id,
app/core/config.py CHANGED
@@ -23,6 +23,10 @@ class Settings(BaseSettings):
23
 
24
  # Google AI
25
  google_api_key: str
 
 
 
 
26
 
27
  # MegaLLM (OpenAI-compatible)
28
  megallm_api_key: str | None = None
 
23
 
24
  # Google AI
25
  google_api_key: str
26
+
27
+ # Google OAuth
28
+ google_client_id: str
29
+ jwt_secret: str
30
 
31
  # MegaLLM (OpenAI-compatible)
32
  megallm_api_key: str | None = None
app/mcp/tools/__init__.py CHANGED
@@ -31,10 +31,15 @@ from app.mcp.tools.graph_tool import (
31
  get_place_details,
32
  get_nearby_by_relationship,
33
  get_same_category_places,
34
- geocode_location,
35
  get_location_coordinates,
36
  TOOL_DEFINITION as GRAPH_TOOL_DEFINITION,
37
  )
 
 
 
 
 
 
38
 
39
 
40
  # Combined tool definitions for agent
@@ -91,6 +96,13 @@ class MCPTools:
91
  async def get_location_coordinates(self, location_name):
92
  """Get coordinates for a location (Neo4j + OSM fallback)."""
93
  return await get_location_coordinates(location_name)
 
 
 
 
 
 
 
94
 
95
 
96
  # Global MCP tools instance
 
31
  get_place_details,
32
  get_nearby_by_relationship,
33
  get_same_category_places,
 
34
  get_location_coordinates,
35
  TOOL_DEFINITION as GRAPH_TOOL_DEFINITION,
36
  )
37
+ from app.mcp.tools.social_tool import (
38
+ SocialSearchResult,
39
+ search_social_media,
40
+ # TODO: Define TOOL_DEFINITION in social_tool if needed for agent JSON schema
41
+ )
42
+
43
 
44
 
45
  # Combined tool definitions for agent
 
96
  async def get_location_coordinates(self, location_name):
97
  """Get coordinates for a location (Neo4j + OSM fallback)."""
98
  return await get_location_coordinates(location_name)
99
+
100
+ # Social Tool
101
+ async def search_social_media(self, query: str, limit: int = 10, freshness: str = "pw", platforms: list[str] = None) -> list[SocialSearchResult]:
102
+ """Search for social media content (news, trends)."""
103
+ return await search_social_media(query, limit, freshness, platforms)
104
+
105
+
106
 
107
 
108
  # Global MCP tools instance
app/mcp/tools/social_tool.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import httpx
4
+ from dataclasses import dataclass
5
+ from typing import Optional, List, Dict, Any
6
+
7
+ @dataclass
8
+ class SocialSearchResult:
9
+ title: str
10
+ url: str
11
+ description: str
12
+ age: str = ""
13
+ platform: str = "Web"
14
+
15
+ class BraveSocialSearch:
16
+ """
17
+ Native Python implementation of Brave Social Search.
18
+ Wraps Brave Search API with social media specific filters.
19
+ """
20
+
21
+ BASE_URL = "https://api.search.brave.com/res/v1/web/search"
22
+
23
+ def __init__(self, api_key: str = None):
24
+ self.api_key = api_key or os.getenv("BRAVE_API_KEY")
25
+ if not self.api_key:
26
+ # Fallback or warning? For now assume it will be provided or env
27
+ pass
28
+
29
+ async def search(self, query: str, limit: int = 10, freshness: str = "pw", platforms: List[str] = None) -> List[SocialSearchResult]:
30
+ if not self.api_key:
31
+ print("Warning: BRAVE_API_KEY not found.")
32
+ return []
33
+
34
+ headers = {
35
+ "Accept": "application/json",
36
+ "Accept-Encoding": "gzip",
37
+ "X-Subscription-Token": self.api_key
38
+ }
39
+
40
+ # Default social sites if none provided
41
+ if not platforms:
42
+ social_sites = [
43
+ 'site:twitter.com', 'site:x.com',
44
+ 'site:facebook.com',
45
+ 'site:reddit.com',
46
+ 'site:linkedin.com',
47
+ 'site:tiktok.com',
48
+ 'site:instagram.com',
49
+ 'site:threads.net'
50
+ ]
51
+ else:
52
+ # Map friendly names to site operators if needed, or assume raw site: checks
53
+ # Simplest is to assume user passes ["facebook", "reddit"] -> ["site:facebook.com", "site:reddit.com"]
54
+ # But let's be robust:
55
+ social_sites = []
56
+ for p in platforms:
57
+ p = p.lower()
58
+ if "facebook" in p: social_sites.append("site:facebook.com")
59
+ elif "reddit" in p: social_sites.append("site:reddit.com")
60
+ elif "twitter" in p or "x" == p: social_sites.extend(["site:twitter.com", "site:x.com"])
61
+ elif "linkedin" in p: social_sites.append("site:linkedin.com")
62
+ elif "tiktok" in p: social_sites.append("site:tiktok.com")
63
+ elif "instagram" in p: social_sites.append("site:instagram.com")
64
+ elif "site:" in p: social_sites.append(p) # Direct operator
65
+
66
+ # Construct query with site OR operator
67
+ if len(social_sites) > 1:
68
+ sites_query = " OR ".join(social_sites)
69
+ full_query = f"{query} ({sites_query})"
70
+ elif len(social_sites) == 1:
71
+ full_query = f"{query} {social_sites[0]}"
72
+ else:
73
+ full_query = query
74
+
75
+ params = {
76
+ "q": full_query,
77
+
78
+
79
+ "count": min(limit, 20),
80
+ "freshness": freshness,
81
+ "result_filter": "web,news,discussions",
82
+ "text_decorations": 0,
83
+ "spellcheck": 1
84
+ }
85
+
86
+ async with httpx.AsyncClient() as client:
87
+ try:
88
+ response = await client.get(
89
+ self.BASE_URL,
90
+ headers=headers,
91
+ params=params,
92
+ timeout=10.0
93
+ )
94
+ response.raise_for_status()
95
+ data = response.json()
96
+
97
+ results = []
98
+
99
+ # Parse 'web' results (most common)
100
+ if "web" in data and "results" in data["web"]:
101
+ for item in data["web"]["results"]:
102
+ # Extract platform from profile or url
103
+ platform = "Web"
104
+ if "profile" in item and "name" in item["profile"]:
105
+ platform = item["profile"]["name"]
106
+ else:
107
+ # Simple heuristic
108
+ domain = item.get("url", "").split("//")[-1].split("/")[0]
109
+ if "reddit" in domain: platform = "Reddit"
110
+ elif "twitter" in domain or "x.com" in domain: platform = "X (Twitter)"
111
+ elif "facebook" in domain: platform = "Facebook"
112
+
113
+ results.append(SocialSearchResult(
114
+ title=item.get("title", ""),
115
+ url=item.get("url", ""),
116
+ description=item.get("description", ""),
117
+ age=item.get("age", ""),
118
+ platform=platform
119
+ ))
120
+
121
+ return results
122
+
123
+ except Exception as e:
124
+ print(f"Error calling Brave Search API: {e}")
125
+ return []
126
+
127
+ # Singleton instance
128
+ social_search_tool = BraveSocialSearch()
129
+
130
+ async def search_social_media(query: str, limit: int = 10, freshness: str = "pw", platforms: List[str] = None) -> List[SocialSearchResult]:
131
+ """
132
+ Search for social media content (news, discussions) about a topic.
133
+ """
134
+ return await social_search_tool.search(query, limit, freshness, platforms)
135
+
docs/PROMPT_REPORT.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LocalMate Backend - Prompt Documentation Report
2
+
3
+ This document contains a comprehensive report of all LLM prompts used within the `localmate-danang-backend-v2` project.
4
+
5
+ ## 1. MMCA Agent (Main Chatbot)
6
+
7
+ **File:** `app/agent/mmca_agent.py`
8
+
9
+ ### System Prompt (`SYSTEM_PROMPT`)
10
+ This is the core instruction for the Multi-Modal Contextual Agent. It defines the available tools and decision-making rules.
11
+
12
+ ```python
13
+ Bạn là trợ lý du lịch thông minh cho Đà Nẵng. Bạn có 3 công cụ tìm kiếm:
14
+
15
+ **1. retrieve_context_text** - Tìm kiếm văn bản thông minh
16
+ - Khi nào: Hỏi về menu, review, mô tả, đặc điểm, phong cách
17
+ - Ví dụ: "Phở ngon giá rẻ", "Quán cafe view đẹp", "Nơi lãng mạn hẹn hò"
18
+ - Đặc biệt: Tự động phát hiện category (cafe, pho, seafood...) và boost kết quả
19
+
20
+ **2. retrieve_similar_visuals** - Tìm theo hình ảnh
21
+ - Khi nào: Người dùng gửi ảnh hoặc mô tả về không gian, decor
22
+ - Scene filter: food, interior, exterior, view
23
+ - Ví dụ: "Quán có không gian giống ảnh này"
24
+
25
+ **3. find_nearby_places** - Tìm theo vị trí
26
+ - Khi nào: Hỏi về khoảng cách, "gần đây", "gần X", "quanh Y"
27
+ - Ví dụ: "Quán cafe gần Cầu Rồng", "Nhà hàng gần bãi biển Mỹ Khê"
28
+ - Đặc biệt: Có thể lấy chi tiết đầy đủ với photos, reviews
29
+
30
+ **4. search_social_media** - Tìm kiếm mạng xã hội và tin tức
31
+ - Khi nào: Hỏi về "review", "tin hot", "trend", "tiktok", "facebook", "tin mới"
32
+ - Ví dụ: "Review quán ăn ngon Đà Nẵng trên TikTok", "Tin hot tuần qua"
33
+ - Tham số: query, freshness ("pw": tuần, "pm": tháng), platforms (["tiktok", "facebook", "reddit"])
34
+
35
+ **Quy tắc quan trọng:**
36
+ 1. Phân tích intent để chọn ĐÚNG tool (không chỉ dùng 1 tool)
37
+ 2. Với câu hỏi tổng quát ("quán cafe ngon") → dùng retrieve_context_text
38
+ 3. Với câu hỏi vị trí ("gần X", "quanh Y") → dùng find_nearby_places
39
+ 4. Với câu hỏi trend/review từ MXH -> dùng search_social_media
40
+ 5. Với ảnh → dùng retrieve_similar_visuals
41
+ 6. Trả lời tiếng Việt, thân thiện, cung cấp thông tin cụ thể (tên, rating, khoảng cách)
42
+ ```
43
+
44
+ ### Synthesis Prompt (`_synthesize_response`)
45
+ Used to generate the final natural language response based on tool outputs.
46
+
47
+ ```python
48
+ {history_section}Dựa trên kết quả tìm kiếm sau, hãy trả lời câu hỏi của người dùng một cách tự nhiên và hữu ích.
49
+
50
+ Câu hỏi hiện tại: {message}
51
+
52
+ {context}
53
+
54
+ Hãy trả lời bằng tiếng Việt, thân thiện. Nếu có nhiều kết quả, hãy giới thiệu top 2-3 địa điểm phù hợp nhất.
55
+ Nếu có lịch sử hội thoại, hãy cân nhắc ngữ cảnh trước đó khi trả lời.
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 2. ReAct Agent (Reasoning Engine)
61
+
62
+ **Files:** `app/agent/react_agent.py` and `app/agent/reasoning.py`
63
+
64
+ ### System Prompt (`REACT_SYSTEM_PROMPT`)
65
+ **File:** `app/agent/reasoning.py`
66
+ Defines the multi-step reasoning capability.
67
+
68
+ ````python
69
+ Bạn là agent du lịch thông minh cho Đà Nẵng với khả năng suy luận multi-step.
70
+
71
+ **Tools có sẵn:**
72
+ 1. `get_location_coordinates` - Lấy tọa độ từ tên địa điểm
73
+ - Input: {"location_name": "Dragon Bridge"}
74
+ - Output: {"lat": 16.06, "lng": 108.22}
75
+
76
+ 2. `find_nearby_places` - Tìm địa điểm gần vị trí
77
+ - Input: {"lat": 16.06, "lng": 108.22, "category": "cafe", "max_distance_km": 3}
78
+ - Output: [{name, category, distance_km, rating}]
79
+
80
+ 3. `retrieve_context_text` - Tìm semantic trong reviews/descriptions
81
+ - Input: {"query": "cafe view đẹp", "limit": 5}
82
+ - Output: [{name, category, rating, source_text}]
83
+
84
+ 4. `retrieve_similar_visuals` - Tìm địa điểm có hình ảnh tương tự
85
+ - Input: {"image_url": "..."}
86
+ - Output: [{name, similarity, image_url}]
87
+
88
+ 5. `search_social_media` - Tìm kiếm mạng xã hội và tin tức
89
+ - Input: {"query": "review quán ăn", "freshness": "pw", "platforms": ["tiktok"]}
90
+ - Output: [{title, url, age, platform}]
91
+
92
+ **Quy trình:**
93
+ Với mỗi bước, bạn phải:
94
+ 1. **Thought**: Suy nghĩ về bước tiếp theo cần làm
95
+ 2. **Action**: Chọn tool hoặc "finish" nếu đủ thông tin
96
+ 3. **Action Input**: JSON parameters cho tool
97
+
98
+ **Trả lời CHÍNH XÁC theo format JSON:**
99
+ ```json
100
+ {
101
+ "thought": "Suy nghĩ của bạn...",
102
+ "action": "tool_name hoặc finish",
103
+ "action_input": {"param1": "value1"}
104
+ }
105
+ ```
106
+
107
+ **Quan trọng:**
108
+ - Nếu cần biết vị trí → dùng get_location_coordinates trước
109
+ - Nếu tìm theo khoảng cách → dùng find_nearby_places
110
+ - Nếu tìm review/trend MXH → dùng search_social_media
111
+ - Nếu cần lọc theo đặc điểm (view, không gian, giá) → dùng retrieve_context_text
112
+ - Khi đủ thông tin → action = "finish"
113
+ ````
114
+
115
+ ### Reasoning Step Prompt (`build_reasoning_prompt`)
116
+ **File:** `app/agent/reasoning.py`
117
+ Dynamic prompt constructed at each step of the ReAct loop.
118
+
119
+ ````python
120
+ **Câu hỏi của user:** {query}
121
+ {image_text}
122
+ {context_summary}
123
+ {steps_text}
124
+ **Bước tiếp theo là gì?**
125
+
126
+ Trả lời theo format JSON:
127
+ ```json
128
+ {{
129
+ "thought": "...",
130
+ "action": "tool_name hoặc finish",
131
+ "action_input": {{...}}
132
+ }}
133
+ ```
134
+ ````
135
+
136
+ ---
137
+
138
+ ## 3. Tool Documentation
139
+
140
+ This section provides a reference for all available tools in the project.
141
+
142
+ | Tool Name | Description | Arguments | Recommended Use |
143
+ | :--- | :--- | :--- | :--- |
144
+ | **`retrieve_context_text`** | Semantic text search using vector embeddings. | `query` (str), `limit` (int) | General queries about place descriptions, reviews, or vague characteristics (e.g., "romance", "good for work"). |
145
+ | **`retrieve_similar_visuals`** | Visual similarity search using CLIP embeddings. | `image_url` (str) or `image_bytes`, `limit` (int) | When the user provides an image or asks to find places looking like X. |
146
+ | **`find_nearby_places`** | Spatial search using Neo4j and Haversine distance. | `lat` (float), `lng` (float), `max_distance_km` (float), `category` (str) | Proximity queries (e.g., "near Dragon Bridge", "around here"). |
147
+ | **`get_location_coordinates`** | Geocoding service (Nominatim + Neo4j fallback). | `location_name` (str) | To convert a location string to lat/lng before searching nearby. |
148
+ | **`search_social_media`** | **[NEW]** Real-time social media and news search via Brave API. | `query` (str), `freshness` (str: "pw", "pm"), `platforms` (list[str]) | Retrieving recent reviews, trending topics, or content from specific platforms like TikTok, Reddit, Facebook. |
scripts/verify_social.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import asyncio
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ # Add app to path
8
+ sys.path.append(str(Path(__file__).parent.parent))
9
+
10
+ from app.mcp.tools import mcp_tools
11
+
12
+ async def main():
13
+ api_key = os.getenv("BRAVE_API_KEY")
14
+ if not api_key:
15
+ print("Error: BRAVE_API_KEY not set")
16
+ return
17
+
18
+ print("Testing Social Search Integration...")
19
+ try:
20
+ results = await mcp_tools.search_social_media("AI Trends", limit=3)
21
+ print(f"Found {len(results)} results:")
22
+ for r in results:
23
+ print(f"- [{r.platform}] {r.title} ({r.age})")
24
+ print(f" Url: {r.url}")
25
+
26
+ except Exception as e:
27
+ print(f"Error: {e}")
28
+
29
+ if __name__ == "__main__":
30
+ asyncio.run(main())