|
|
@@ -1,215 +1,144 @@
|
|
|
"""
|
|
|
-热点宝画像数据工具
|
|
|
+热点宝画像数据工具(示例)
|
|
|
+
|
|
|
+调用内部爬虫服务获取账号/内容的粉丝画像。
|
|
|
"""
|
|
|
+import asyncio
|
|
|
+import json
|
|
|
+from typing import Optional
|
|
|
|
|
|
-from typing import Dict, Any, Optional, List
|
|
|
-from agent.tools import tool, ToolResult, ToolContext
|
|
|
+import httpx
|
|
|
|
|
|
+from agent.tools import tool, ToolResult
|
|
|
|
|
|
-@tool(description="获取抖音视频的点赞观众画像数据")
|
|
|
-async def get_video_audience_profile(
|
|
|
- video_id: str,
|
|
|
- ctx: Optional[ToolContext] = None,
|
|
|
-) -> ToolResult:
|
|
|
- """
|
|
|
- 获取抖音视频的点赞观众画像数据
|
|
|
|
|
|
- ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
|
|
|
- 实际使用时需要对接真实的热点宝API
|
|
|
+ACCOUNT_FANS_PORTRAIT_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/re_dian_bao/account_fans_portrait"
|
|
|
+CONTENT_FANS_PORTRAIT_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/re_dian_bao/video_like_portrait"
|
|
|
+DEFAULT_TIMEOUT = 60.0
|
|
|
|
|
|
- 包括:
|
|
|
- - 观众地域分布(占比&偏好度)
|
|
|
- - 观众城市等级(占比&偏好度)
|
|
|
- - 观众性别分布(占比&偏好度)
|
|
|
- - 观众年龄分布(占比&偏好度)
|
|
|
|
|
|
- Args:
|
|
|
- video_id: 抖音视频ID
|
|
|
- ctx: 工具上下文
|
|
|
-
|
|
|
- Returns:
|
|
|
- ToolResult: 包含观众画像数据(模拟数据)
|
|
|
+@tool(description="获取抖音账号粉丝画像(热点宝),支持选择画像维度")
|
|
|
+async def get_account_fans_portrait(
|
|
|
+ account_id: str,
|
|
|
+ need_province: bool = False,
|
|
|
+ need_city: bool = False,
|
|
|
+ need_city_level: bool = False,
|
|
|
+ need_gender: bool = False,
|
|
|
+ need_age: bool = True,
|
|
|
+ need_phone_brand: bool = False,
|
|
|
+ need_phone_price: bool = False,
|
|
|
+ timeout: Optional[float] = None,
|
|
|
+) -> ToolResult:
|
|
|
"""
|
|
|
- # TODO: 实际实现需要调用热点宝API
|
|
|
- # 这里返回模拟数据
|
|
|
-
|
|
|
- profile = await _mock_video_audience_profile(video_id)
|
|
|
-
|
|
|
- # 构建输出
|
|
|
- output_lines = [
|
|
|
- f"⚠️ [模拟数据] 视频 {video_id} 的点赞观众画像:",
|
|
|
- "",
|
|
|
- "⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实热点宝API。",
|
|
|
- "",
|
|
|
- "【年龄分布】",
|
|
|
- ]
|
|
|
-
|
|
|
- for age_range, data in profile["age_distribution"].items():
|
|
|
- output_lines.append(f" {age_range}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【性别分布】",
|
|
|
- ])
|
|
|
- for gender, data in profile["gender_distribution"].items():
|
|
|
- output_lines.append(f" {gender}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【城市等级】",
|
|
|
- ])
|
|
|
- for city_level, data in profile["city_level_distribution"].items():
|
|
|
- output_lines.append(f" {city_level}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【地域分布 TOP5】",
|
|
|
- ])
|
|
|
- for i, (region, data) in enumerate(list(profile["region_distribution"].items())[:5], 1):
|
|
|
- output_lines.append(f" {i}. {region}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- return ToolResult(
|
|
|
- title=f"[模拟数据] 视频观众画像: {video_id}",
|
|
|
- output="\n".join(output_lines),
|
|
|
- metadata={**profile, "is_mock_data": True, "warning": "这是模拟数据,实际使用需对接真实API"},
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-@tool(description="获取抖音用户的粉丝画像数据")
|
|
|
-async def get_user_fans_profile(
|
|
|
- author_id: str,
|
|
|
- ctx: Optional[ToolContext] = None,
|
|
|
+ 获取抖音账号粉丝画像
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ payload = {
|
|
|
+ "account_id": account_id,
|
|
|
+ "need_province": need_province,
|
|
|
+ "need_city": need_city,
|
|
|
+ "need_city_level": need_city_level,
|
|
|
+ "need_gender": need_gender,
|
|
|
+ "need_age": need_age,
|
|
|
+ "need_phone_brand": need_phone_brand,
|
|
|
+ "need_phone_price": need_phone_price,
|
|
|
+ }
|
|
|
+
|
|
|
+ request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
|
+
|
|
|
+ async with httpx.AsyncClient(timeout=request_timeout) as client:
|
|
|
+ response = await client.post(
|
|
|
+ ACCOUNT_FANS_PORTRAIT_API,
|
|
|
+ json=payload,
|
|
|
+ headers={"Content-Type": "application/json"},
|
|
|
+ )
|
|
|
+ response.raise_for_status()
|
|
|
+ data = response.json()
|
|
|
+
|
|
|
+ return ToolResult(
|
|
|
+ title=f"账号粉丝画像: {account_id}",
|
|
|
+ output=json.dumps(data, ensure_ascii=False, indent=2),
|
|
|
+ long_term_memory=f"Fetched fans portrait for account '{account_id}'",
|
|
|
+ )
|
|
|
+ except httpx.HTTPStatusError as e:
|
|
|
+ return ToolResult(
|
|
|
+ title="账号粉丝画像获取失败",
|
|
|
+ output="",
|
|
|
+ error=f"HTTP error {e.response.status_code}: {e.response.text}",
|
|
|
+ )
|
|
|
+ except Exception as e:
|
|
|
+ return ToolResult(
|
|
|
+ title="账号粉丝画像获取失败",
|
|
|
+ output="",
|
|
|
+ error=str(e),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+@tool(description="获取抖音内容粉丝画像(热点宝),支持选择画像维度")
|
|
|
+async def get_content_fans_portrait(
|
|
|
+ content_id: str,
|
|
|
+ need_province: bool = False,
|
|
|
+ need_city: bool = False,
|
|
|
+ need_city_level: bool = False,
|
|
|
+ need_gender: bool = False,
|
|
|
+ need_age: bool = True,
|
|
|
+ need_phone_brand: bool = False,
|
|
|
+ need_phone_price: bool = False,
|
|
|
+ timeout: Optional[float] = None,
|
|
|
) -> ToolResult:
|
|
|
"""
|
|
|
- 获取抖音用户的粉丝画像数据
|
|
|
-
|
|
|
- ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
|
|
|
- 实际使用时需要对接真实的热点宝API
|
|
|
-
|
|
|
- 包括:
|
|
|
- - 粉丝地域分布(占比&偏好度)
|
|
|
- - 粉丝城市等级(占比&偏好度)
|
|
|
- - 粉丝性别分布(占比&偏好度)
|
|
|
- - 粉丝年龄分布(占比&偏好度)
|
|
|
-
|
|
|
- Args:
|
|
|
- author_id: 抖音用户ID
|
|
|
- ctx: 工具上下文
|
|
|
-
|
|
|
- Returns:
|
|
|
- ToolResult: 包含粉丝画像数据(模拟数据)
|
|
|
+ 获取抖音内容粉丝画像
|
|
|
"""
|
|
|
- # TODO: 实际实现需要调用热点宝API
|
|
|
- # 这里返回模拟数据
|
|
|
-
|
|
|
- profile = await _mock_user_fans_profile(author_id)
|
|
|
-
|
|
|
- # 构建输出
|
|
|
- output_lines = [
|
|
|
- f"⚠️ [模拟数据] 用户 {author_id} 的粉丝画像:",
|
|
|
- "",
|
|
|
- "⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实热点宝API。",
|
|
|
- "",
|
|
|
- f"粉丝总数: {profile['total_fans']:,}",
|
|
|
- "",
|
|
|
- "【年龄分布】",
|
|
|
- ]
|
|
|
-
|
|
|
- for age_range, data in profile["age_distribution"].items():
|
|
|
- output_lines.append(f" {age_range}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【性别分布】",
|
|
|
- ])
|
|
|
- for gender, data in profile["gender_distribution"].items():
|
|
|
- output_lines.append(f" {gender}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【城市等级】",
|
|
|
- ])
|
|
|
- for city_level, data in profile["city_level_distribution"].items():
|
|
|
- output_lines.append(f" {city_level}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- output_lines.extend([
|
|
|
- "",
|
|
|
- "【地域分布 TOP5】",
|
|
|
- ])
|
|
|
- for i, (region, data) in enumerate(list(profile["region_distribution"].items())[:5], 1):
|
|
|
- output_lines.append(f" {i}. {region}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
|
|
|
-
|
|
|
- return ToolResult(
|
|
|
- title=f"[模拟数据] 用户粉丝画像: {author_id}",
|
|
|
- output="\n".join(output_lines),
|
|
|
- metadata={**profile, "is_mock_data": True, "warning": "这是模拟数据,实际使用需对接真实API"},
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-async def _mock_video_audience_profile(video_id: str) -> Dict[str, Any]:
|
|
|
- """模拟视频观众画像数据"""
|
|
|
- return {
|
|
|
- "video_id": video_id,
|
|
|
- "age_distribution": {
|
|
|
- "18-24岁": {"percentage": 15.2, "preference": 0.85},
|
|
|
- "25-30岁": {"percentage": 22.5, "preference": 1.05},
|
|
|
- "31-40岁": {"percentage": 28.3, "preference": 1.15},
|
|
|
- "41-50岁": {"percentage": 20.8, "preference": 1.25},
|
|
|
- "51-60岁": {"percentage": 10.5, "preference": 1.45},
|
|
|
- "60岁以上": {"percentage": 2.7, "preference": 1.65},
|
|
|
- },
|
|
|
- "gender_distribution": {
|
|
|
- "男性": {"percentage": 58.5, "preference": 1.12},
|
|
|
- "女性": {"percentage": 41.5, "preference": 0.88},
|
|
|
- },
|
|
|
- "city_level_distribution": {
|
|
|
- "一线城市": {"percentage": 18.5, "preference": 0.92},
|
|
|
- "新一线城市": {"percentage": 25.3, "preference": 1.05},
|
|
|
- "二线城市": {"percentage": 28.7, "preference": 1.15},
|
|
|
- "三线城市": {"percentage": 18.2, "preference": 1.08},
|
|
|
- "四线及以下": {"percentage": 9.3, "preference": 0.95},
|
|
|
- },
|
|
|
- "region_distribution": {
|
|
|
- "广东": {"percentage": 12.5, "preference": 1.08},
|
|
|
- "江苏": {"percentage": 9.8, "preference": 1.12},
|
|
|
- "山东": {"percentage": 8.5, "preference": 1.05},
|
|
|
- "浙江": {"percentage": 7.2, "preference": 1.15},
|
|
|
- "河南": {"percentage": 6.8, "preference": 0.98},
|
|
|
- "四川": {"percentage": 5.5, "preference": 1.02},
|
|
|
- },
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-async def _mock_user_fans_profile(author_id: str) -> Dict[str, Any]:
|
|
|
- """模拟用户粉丝画像数据"""
|
|
|
- return {
|
|
|
- "author_id": author_id,
|
|
|
- "total_fans": 125000,
|
|
|
- "age_distribution": {
|
|
|
- "18-24岁": {"percentage": 12.5, "preference": 0.78},
|
|
|
- "25-30岁": {"percentage": 18.8, "preference": 0.95},
|
|
|
- "31-40岁": {"percentage": 25.5, "preference": 1.08},
|
|
|
- "41-50岁": {"percentage": 24.2, "preference": 1.28},
|
|
|
- "51-60岁": {"percentage": 14.5, "preference": 1.52},
|
|
|
- "60岁以上": {"percentage": 4.5, "preference": 1.85},
|
|
|
- },
|
|
|
- "gender_distribution": {
|
|
|
- "男性": {"percentage": 62.5, "preference": 1.18},
|
|
|
- "女性": {"percentage": 37.5, "preference": 0.82},
|
|
|
- },
|
|
|
- "city_level_distribution": {
|
|
|
- "一线城市": {"percentage": 15.2, "preference": 0.88},
|
|
|
- "新一线城市": {"percentage": 22.8, "preference": 1.02},
|
|
|
- "二线城市": {"percentage": 30.5, "preference": 1.18},
|
|
|
- "三线城市": {"percentage": 20.5, "preference": 1.12},
|
|
|
- "四线及以下": {"percentage": 11.0, "preference": 1.05},
|
|
|
- },
|
|
|
- "region_distribution": {
|
|
|
- "广东": {"percentage": 11.8, "preference": 1.05},
|
|
|
- "江苏": {"percentage": 10.2, "preference": 1.15},
|
|
|
- "山东": {"percentage": 9.5, "preference": 1.08},
|
|
|
- "浙江": {"percentage": 7.8, "preference": 1.18},
|
|
|
- "河南": {"percentage": 7.2, "preference": 1.02},
|
|
|
- "四川": {"percentage": 6.5, "preference": 1.08},
|
|
|
- },
|
|
|
- }
|
|
|
+ try:
|
|
|
+ payload = {
|
|
|
+ "content_id": content_id,
|
|
|
+ "need_province": need_province,
|
|
|
+ "need_city": need_city,
|
|
|
+ "need_city_level": need_city_level,
|
|
|
+ "need_gender": need_gender,
|
|
|
+ "need_age": need_age,
|
|
|
+ "need_phone_brand": need_phone_brand,
|
|
|
+ "need_phone_price": need_phone_price,
|
|
|
+ }
|
|
|
+
|
|
|
+ request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
|
+
|
|
|
+ async with httpx.AsyncClient(timeout=request_timeout) as client:
|
|
|
+ response = await client.post(
|
|
|
+ CONTENT_FANS_PORTRAIT_API,
|
|
|
+ json=payload,
|
|
|
+ headers={"Content-Type": "application/json"},
|
|
|
+ )
|
|
|
+ response.raise_for_status()
|
|
|
+ data = response.json()
|
|
|
+
|
|
|
+ return ToolResult(
|
|
|
+ title=f"内容粉丝画像: {content_id}",
|
|
|
+ output=json.dumps(data, ensure_ascii=False, indent=2),
|
|
|
+ long_term_memory=f"Fetched fans portrait for content '{content_id}'",
|
|
|
+ )
|
|
|
+ except httpx.HTTPStatusError as e:
|
|
|
+ return ToolResult(
|
|
|
+ title="内容粉丝画像获取失败",
|
|
|
+ output="",
|
|
|
+ error=f"HTTP error {e.response.status_code}: {e.response.text}",
|
|
|
+ )
|
|
|
+ except Exception as e:
|
|
|
+ return ToolResult(
|
|
|
+ title="内容粉丝画像获取失败",
|
|
|
+ output="",
|
|
|
+ error=str(e),
|
|
|
+ )
|
|
|
+
|
|
|
+# async def main():
|
|
|
+# # result = await get_account_fans_portrait(
|
|
|
+# # account_id="MS4wLjABAAAAXvRdWJsdPKkh9Ja3ZirxoB8pAaxNXUXs1KUe14gW0IoqDz-D-fG0xZ8c5kSfTPXx"
|
|
|
+# # )
|
|
|
+# # print(result.output)
|
|
|
+# result = await get_content_fans_portrait(
|
|
|
+# content_id="7495776724350405928"
|
|
|
+# )
|
|
|
+# print(result.output)
|
|
|
+#
|
|
|
+# if __name__ == "__main__":
|
|
|
+# asyncio.run(main())
|