Compare commits

..

13 Commits

14 changed files with 2591 additions and 184 deletions

View File

@@ -0,0 +1,40 @@
name: 手动 LC python main 分支构建部署
on:
workflow_dispatch:
jobs:
scheduled_deploy:
runs-on: ubuntu-latest
env:
REMOTE_DEPLOY_PATH: /workspace/Trinity/Litserve_LC_Prod/lc_stylist_agent
steps:
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: 'main'
- name: 2.复制文件到服务器
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
password: ${{ secrets.SERVER_PASSWORD }}
source: "."
target: ${{ env.REMOTE_DEPLOY_PATH }}
- name: 3.重启docker-compose
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
password: ${{ secrets.SERVER_PASSWORD }}
script: |
# 进入项目目录
cd ${{ env.REMOTE_DEPLOY_PATH }}
docker-compose down 2>&1
docker-compose up -d --build --remove-orphans 2>&1
# docker image prune -f 2>&1

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ data/
.prod_env .prod_env
google_application_credentials.json google_application_credentials.json
*.bash *.bash
test
app/google_application_credentials.json

View File

@@ -1,46 +1,37 @@
# Change CUDA and cuDNN version here FROM ghcr.io/astral-sh/uv:latest AS uv_bin
FROM nvidia/cuda:12.4.1-base-ubuntu22.04 FROM nvidia/cuda:12.4.1-base-ubuntu22.04
ARG PYTHON_VERSION=3.10
ENV DEBIAN_FRONTEND=noninteractive # 1. 基础环境配置
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENVIRONMENT=/app/.venv
COPY --from=uv_bin /uv /uvx /bin/
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common \
wget \ wget \
&& add-apt-repository ppa:deadsnakes/ppa \ libcurl4-openssl-dev \
&& apt-get update && apt-get install -y --no-install-recommends \ build-essential \
python$PYTHON_VERSION \ libgl1 \
python$PYTHON_VERSION-dev \ libglib2.0-0 \
python$PYTHON_VERSION-venv \ ca-certificates \
&& wget https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python$PYTHON_VERSION get-pip.py \
&& rm get-pip.py \
&& ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python \
&& ln -sf /usr/local/bin/pip$PYTHON_VERSION /usr/local/bin/pip \
&& python --version \
&& pip --version \
&& apt-get purge -y --auto-remove software-properties-common \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
####### Add your own installation commands here #######
# RUN pip install some-package
# RUN wget https://path/to/some/data/or/weights
# RUN apt-get update && apt-get install -y <package-name>
RUN apt-get update && \
apt-get install -y libcurl4-openssl-dev build-essential
RUN apt-get update
RUN apt-get -y install libgl1
RUN apt-get -y install libglib2.0-0
WORKDIR /app WORKDIR /app
COPY . /app
# Install litserve and requirements COPY pyproject.toml uv.lock ./
RUN pip install --upgrade pip setuptools wheel
RUN pip install --no-cache-dir litserve==0.2.16 -r requirements.txt ENV UV_COMPILE_BYTECODE=0
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
RUN uv sync --frozen --no-dev --no-install-project --python 3.10
# 4. 拷贝项目文件并安装项目本身
COPY . .
RUN uv sync --frozen --no-dev --python 3.10
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000 EXPOSE 8000
CMD ["python", "-m","app.main"] CMD ["uv", "run","-m","app.main"]
#CMD ["tail", "-f","/dev/null"]

View File

@@ -16,6 +16,8 @@ class Settings(BaseSettings):
env_file_encoding='utf-8', env_file_encoding='utf-8',
extra='ignore' # 忽略环境变量中多余的键 extra='ignore' # 忽略环境变量中多余的键
) )
# 启动端口
SERVE_PROD: int = Field(default=8000, description='')
# 调试配饰 # 调试配饰
LOCAL: int = Field(default=0, description="是否在本地运行1表示本地运行0表示生产环境运行") LOCAL: int = Field(default=0, description="是否在本地运行1表示本地运行0表示生产环境运行")

View File

@@ -27,4 +27,4 @@ if __name__ == "__main__":
agent_api = LCAgent(enable_async=True, api_path='/api/v1/agent') agent_api = LCAgent(enable_async=True, api_path='/api/v1/agent')
reface_api = ReFace(api_path='/api/v1/reface') reface_api = ReFace(api_path='/api/v1/reface')
server = ls.LitServer([chat_boot_api, agent_api, reface_api]) server = ls.LitServer([chat_boot_api, agent_api, reface_api])
server.run(port=8000) server.run(port=settings.SERVE_PROD)

View File

@@ -1,8 +1,9 @@
import asyncio import asyncio
import json
import logging import logging
import uuid import uuid
from enum import Enum from enum import Enum
from typing import List from typing import List, Optional
from pydantic import Field from pydantic import Field
import time import time
@@ -15,6 +16,7 @@ from app.server.ChatbotAgent.core.redis_manager import RedisManager
from app.server.ChatbotAgent.core.stylist_agent_server import AsyncStylistAgent from app.server.ChatbotAgent.core.stylist_agent_server import AsyncStylistAgent
from app.server.ChatbotAgent.core.prompt import SUMMARY_PROMPT from app.server.ChatbotAgent.core.prompt import SUMMARY_PROMPT
from app.server.ChatbotAgent.core.vector_database import VectorDatabase from app.server.ChatbotAgent.core.vector_database import VectorDatabase
from app.server.utils.request_post import post_request
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -56,6 +58,8 @@ class AgentRequestModel(BaseModel):
batch_sources: List[str] batch_sources: List[str]
callback_url: str callback_url: str
gender: str gender: str
occasions: Optional[list] = None
request_summary: Optional[str] = None
class LCAgent(ls.LitAPI): class LCAgent(ls.LitAPI):
@@ -105,8 +109,12 @@ class LCAgent(ls.LitAPI):
async def background_run(self, request: AgentRequestModel, outfit_ids): async def background_run(self, request: AgentRequestModel, outfit_ids):
# 1. 根据用户ID查询对话历史总结对话内容 # 1. 根据用户ID查询对话历史总结对话内容
if request.request_summary and request.occasions:
request_summary = request.request_summary
occasions = request.occasions
else:
request_summary, occasions = await self.get_conversation_summary(request.session_id) request_summary, occasions = await self.get_conversation_summary(request.session_id)
logger.info(f"request_summary: {request_summary}") logger.info(f"request_summary: {request_summary},occasions : {occasions}")
# 2.根据对话总结推荐搭配 # 2.根据对话总结推荐搭配
recommendation_results = await self.recommend_outfit( recommendation_results = await self.recommend_outfit(
@@ -138,7 +146,7 @@ class LCAgent(ls.LitAPI):
history_messages = self.redis.get_history(session_id) history_messages = self.redis.get_history(session_id)
if not history_messages: if not history_messages:
# 处理无历史记录的情况 # 处理无历史记录的情况
return {"occasions": [], "summary": "User has no history provided."} return "User has no history provided.", []
input_message = "\n".join([f"{msg.role.value}: {msg.content}" for msg in history_messages]) input_message = "\n".join([f"{msg.role.value}: {msg.content}" for msg in history_messages])
json_schema = StylistResponse.model_json_schema() json_schema = StylistResponse.model_json_schema()
@@ -187,32 +195,26 @@ class LCAgent(ls.LitAPI):
task_map = {} task_map = {}
stylist_agent_kwages = self.stylist_agent_kwages.copy() stylist_agent_kwages = self.stylist_agent_kwages.copy()
tasks_mapping = {}
if num_outfits == 1: if num_outfits == 1:
# 通过请求数量判断 num == 1 单个outfit刷新 tasks_mapping[outfit_ids[0]] = "fast"
stylist_agent_kwages['outfit_id'] = outfit_ids[0] else:
stylist_agent_kwages['stylist_name'] = stylist_name
stylist_agent_kwages['gender'] = gender
agent = AsyncStylistAgent(**stylist_agent_kwages)
task = agent.run_iterative_styling(
request_summary=request_summary,
occasions=occasions,
start_outfit=start_outfit,
batch_sources=batch_sources,
user_id=user_id,
callback_url=callback_url,
)
tasks.append(task)
task_map[task] = {"outfit_id": outfit_ids[0], "retries": 0}
elif num_outfits > 1:
# 通过请求数量判断 num > 1 四套搭配推荐 (1快 , num-1慢)
for i in range(num_outfits): for i in range(num_outfits):
stylist_agent_kwages['outfit_id'] = outfit_ids[i] if i == 0:
tasks_mapping[outfit_ids[i]] = "fast"
else:
tasks_mapping[outfit_ids[i]] = "slow"
for k, v in tasks_mapping.items():
logger.info(f"fast request outfit_id is : {k}")
# 通过请求数量判断 num == 1 单个outfit刷新
stylist_agent_kwages['outfit_id'] = k
stylist_agent_kwages['stylist_name'] = stylist_name stylist_agent_kwages['stylist_name'] = stylist_name
stylist_agent_kwages['gender'] = gender stylist_agent_kwages['gender'] = gender
stylist_agent_kwages['callback_url'] = callback_url
agent = AsyncStylistAgent(**stylist_agent_kwages) agent = AsyncStylistAgent(**stylist_agent_kwages)
if i == 0: if v == "fast":
# 第一套搭配使用快速方法 一次跑出所有单品
logger.info(f"fast request outfit_id is : {outfit_ids[i]}")
task = agent.run_quick_batch_styling( task = agent.run_quick_batch_styling(
request_summary=request_summary, request_summary=request_summary,
occasions=occasions, occasions=occasions,
@@ -222,7 +224,6 @@ class LCAgent(ls.LitAPI):
callback_url=callback_url, callback_url=callback_url,
) )
else: else:
# 后续
task = agent.run_iterative_styling( task = agent.run_iterative_styling(
request_summary=request_summary, request_summary=request_summary,
occasions=occasions, occasions=occasions,
@@ -232,7 +233,7 @@ class LCAgent(ls.LitAPI):
callback_url=callback_url, callback_url=callback_url,
) )
tasks.append(task) tasks.append(task)
task_map[task] = {"outfit_id": outfit_ids[i], "retries": 0} task_map[task] = {"outfit_id": k, "retries": 0}
logger.info(f"--- Starting {num_outfits} concurrent outfit generation tasks. ---") logger.info(f"--- Starting {num_outfits} concurrent outfit generation tasks. ---")
# 2. 任务执行与重试循环 # 2. 任务执行与重试循环
@@ -243,7 +244,8 @@ class LCAgent(ls.LitAPI):
retry_limit = 1 # 允许重试一次 retry_limit = 1 # 允许重试一次
while tasks_to_run: while tasks_to_run:
try: try:
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks_to_run, return_exceptions=True)
next_tasks_to_run = [] next_tasks_to_run = []
for task, result in zip(tasks_to_run, results): for task, result in zip(tasks_to_run, results):
task_info = task_map[task] task_info = task_map[task]
@@ -255,7 +257,14 @@ class LCAgent(ls.LitAPI):
logger.error(f"Outfit {outfit_id} failed with error: {result}. Current retries: {current_retries}.") logger.error(f"Outfit {outfit_id} failed with error: {result}. Current retries: {current_retries}.")
if current_retries < retry_limit: if current_retries < retry_limit:
# 尚未达到重试上限,准备重试 # 尚未达到重试上限,准备重试 并通知前端
object_data = {
'outfit_id': outfit_id,
"status": "retrying",
"path": "",
}
post_request(url=f'{callback_url}/api/style/callback', data=json.dumps(object_data))
task_info["retries"] += 1 task_info["retries"] += 1
logger.info(f"--- Retrying outfit {outfit_id} (Attempt {current_retries + 1}/{retry_limit}). ---") logger.info(f"--- Retrying outfit {outfit_id} (Attempt {current_retries + 1}/{retry_limit}). ---")
@@ -263,8 +272,20 @@ class LCAgent(ls.LitAPI):
stylist_agent_kwages['outfit_id'] = outfit_id stylist_agent_kwages['outfit_id'] = outfit_id
stylist_agent_kwages['stylist_name'] = stylist_name stylist_agent_kwages['stylist_name'] = stylist_name
stylist_agent_kwages['gender'] = gender stylist_agent_kwages['gender'] = gender
stylist_agent_kwages['callback_url'] = callback_url
agent = AsyncStylistAgent(**stylist_agent_kwages) agent = AsyncStylistAgent(**stylist_agent_kwages)
if tasks_mapping[outfit_id] == "fast":
new_task = agent.run_quick_batch_styling( new_task = agent.run_quick_batch_styling(
request_summary=request_summary,
occasions=occasions,
start_outfit=start_outfit,
batch_sources=batch_sources,
user_id=user_id,
callback_url=callback_url,
)
else:
new_task = agent.run_iterative_styling(
request_summary=request_summary, request_summary=request_summary,
occasions=occasions, occasions=occasions,
start_outfit=start_outfit, start_outfit=start_outfit,
@@ -279,8 +300,15 @@ class LCAgent(ls.LitAPI):
# 清理旧任务(可选,但推荐,以防内存泄漏或混淆) # 清理旧任务(可选,但推荐,以防内存泄漏或混淆)
del task_map[task] del task_map[task]
else: else:
# 达到重试上限,最终失败 # 达到重试上限,最终失败 并通知前端
object_data = {
'outfit_id': outfit_id,
"status": "retry_failed",
"path": "",
}
response = post_request(url=f'{callback_url}/api/style/callback', data=json.dumps(object_data))
failed_outfits.append(f"Outfit {outfit_id} ultimately failed after {retry_limit} retries: {result}") failed_outfits.append(f"Outfit {outfit_id} ultimately failed after {retry_limit} retries: {result}")
logger.info(f"request data {json.dumps(object_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}")
del task_map[task] del task_map[task]
else: else:
@@ -320,14 +348,19 @@ if __name__ == "__main__":
# 2. 准备请求数据 # 2. 准备请求数据
import json import json
stylist_agent_kwages = agent_api.stylist_agent_kwages.copy() stylist_agent_kwages = agent_api.stylist_agent_kwages.copy()
with open("/mnt/data/workspace/Code/lc_stylist_agent/data/2025_q4/request_test.json", "r") as f: if settings.LOCAL == 1:
request_file_path = "./data/2025_q4/request_test.json"
else:
request_file_path = "/mnt/data/workspace/Code/lc_stylist_agent/data/2025_q4/request_test.json"
with open(request_file_path, "r") as f:
request_data = json.load(f) request_data = json.load(f)
tasks_with_metadata = [] tasks_with_metadata = []
for test_content in request_data[20:25]: for test_content in request_data[14:20]:
occasions = test_content['occasions'] occasions = test_content['occasions']
request_summary = test_content['request_summary'] request_summary = test_content['request_summary']
for stylist_name in ["edi", "vera"]: for stylist_name in ["edi"]:
stylist_agent_kwages['outfit_id'] = test_content['test_case_id'] + "_" + "_".join(occasions) + f"_{stylist_name}" stylist_agent_kwages['outfit_id'] = test_content['test_case_id'] + "_" + "_".join(occasions) + f"_{stylist_name}"
stylist_agent_kwages['stylist_name'] = stylist_name stylist_agent_kwages['stylist_name'] = stylist_name
stylist_agent_kwages['gender'] = "female" stylist_agent_kwages['gender'] = "female"

View File

@@ -1,9 +1,13 @@
import logging import logging
import sys
import litserve as ls import litserve as ls
from typing import AsyncGenerator from typing import AsyncGenerator
from google import genai from google import genai
from pydantic import BaseModel from pydantic import BaseModel
from sympy.core.evalf import rnd
from app.config import settings from app.config import settings
from google.genai import types from google.genai import types
@@ -95,42 +99,61 @@ class LCChatBot(ls.LitAPI):
if __name__ == "__main__": if __name__ == "__main__":
sys.stdout = open('permanent.log', 'w', encoding='utf-8')
import asyncio import asyncio
async def run_simple_test():
async def run_simple_test(text):
""" """
一个简单的异步测试用例,用于测试 LCChatBot 的流式输出。 一个简单的异步测试用例,用于测试 LCChatBot 的流式输出。
""" """
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("--- 🔬 开始 LCChatBot 简单流式测试 ---") # print("--- 🔬 开始 LCChatBot 简单流式测试 ---")
# 1. 初始化 LitAPI 和其依赖 # 1. 初始化 LitAPI 和其依赖
chatbot_api = LCChatBot() chatbot_api = LCChatBot()
chatbot_api.setup(device="cpu") chatbot_api.setup(device="cpu")
print("✅ Setup complete. Mock services initialized.") # print("✅ Setup complete. Mock services initialized.")
# 2. 构造请求数据 # 2. 构造请求数据
request_data = PredictRequest( request_data = PredictRequest(
user_id="simple_user", user_id="simple_user",
session_id="simple_session", session_id="simple_session",
user_message="I want an outfit. I am going to a evening party with friends. Suggest something stylish yet comfortable.", user_message=text,
gender="female" gender="female"
) )
chatbot_api.redis.clear_history(request_data.session_id) chatbot_api.redis.clear_history(request_data.session_id)
print(f"-> 正在发送查询: {request_data.user_message}") print(f"user: \n {request_data.user_message}")
# 3. 调用 predict 方法并处理流 # 3. 调用 predict 方法并处理流
response_generator = chatbot_api.predict(request_data) response_generator = chatbot_api.predict(request_data)
print("\n<- 接收流式响应:") print("agent:")
# 4. 异步迭代生成器,实时打印输出 # 4. 异步迭代生成器,实时打印输出
async for chunk in response_generator: async for chunk in response_generator:
print(chunk, end="", flush=True) print(chunk, end="", flush=True)
print("\n" + "=" * 50)
# 启动异步事件循环 text_list = [
try: '我要去参加好朋友的婚礼,你能帮我挑一套衣服吗?',
asyncio.run(run_simple_test()) 'I need something to wear for a big presentation at work tomorrow. I want to look powerful but still approachable.',
except Exception as e: 'Who do you think is the best world leader right now?',
print(f"\n发生致命错误: {e}") 'Im going on a trip to Paris next week and need some outfits.',
'Help me find a cool outfit for a rock concert. I hate wearing dresses.',
'I want to look very cool, 或者是那种很有个性的风格 for a gallery opening.',
"I'm going to a gala. Please list 5 different dress styles for me and use bold text for the names.",
"I'm feeling really sad today and just want an outfit that matches my mood."
]
for text in text_list:
asyncio.run(run_simple_test(text))
# print("\n" + "=" * 50)
# # 启动异步事件循环
# try:
# asyncio.run(run_simple_test())
# except Exception as e:
# print(f"\n发生致命错误: {e}")
#
sys.stdout.close()

View File

@@ -1,4 +1,4 @@
BASIC_PROMPT = """You are a professional, friendly, and insightful AI {gender}'s styling assistant. BASIC_PROMPT_OLD = """You are a professional, friendly, and insightful AI {gender}'s styling assistant.
Your primary mission is to engage in a multi-turn conversation with the user to fully understand their dressing intent. You must adopt a professional yet approachable tone. Your primary mission is to engage in a multi-turn conversation with the user to fully understand their dressing intent. You must adopt a professional yet approachable tone.
@@ -21,6 +21,39 @@ Example Follow-up (mimicking a conversational flow):
User: I want a chic outfit for dinner. User: I want a chic outfit for dinner.
Your Response: Hey there! A chic dinner outfit, I love that! To give you the perfect recommendations, tell me: is this a romantic date, business dinner, or celebration with friends? And what's your go-to style vibe: classic elegance or something with more edge?""" Your Response: Hey there! A chic dinner outfit, I love that! To give you the perfect recommendations, tell me: is this a romantic date, business dinner, or celebration with friends? And what's your go-to style vibe: classic elegance or something with more edge?"""
BASIC_PROMPT = """
You are a professional, friendly, and insightful AI {gender}'s styling assistant. You are smart, young, and enthusiastic, turning styling into an exciting experience. Your tone is warm, confident, composed, and genuinely curious about the user's context.
CONVERSATION GOALS:
1. **Occasion:** Determine the specific event (e.g., romantic dinner, summer wedding).
2. **Style:** Pinpoint the desired aesthetic (e.g., minimalist, bohemian).
3. **Vibe/Details:** Gather mood or constraints (e.g., comfort, specific colors).
4. **Item Preference:** Identify preferences for silhouettes or specific items.
PERSONALITY AND COMMUNICATION RULES:
- Always reply in English only. If the user speaks another language, state in English that you cannot speak that language and steer back to styling.
- Keep responses extremely SHORT (maximum 2 sentences).
- Use clear, modern, and vivid language. Avoid clichés or corporate jargon.
- Be positive and encouraging, never patronizing.
- When uncertain, state assumptions and invite correction.
- Gather sufficient intent before providing recommendations. Ask only 1 to 2 essential questions per turn.
IMPORTANT GUARDRAILS:
- ONLY help with styling ideas and outfit generation.
- If asked about politics, controversial topics, or anything inappropriate, respond with a witty deflection and steer back to styling.
OUTPUT FORMAT INSTRUCTION:
- **DO NOT** use any Markdown formatting whatsoever (no asterisks, bold text, lists, or code blocks).
- **ONLY** output the plain text response spoken by the AI Assistant.
EXAMPLE DIALOGUES:
User: 我想找件衣服参加婚礼。 Response: I am so sorry, but I can only speak English! I would love to help you find the perfect wedding guest look if you can tell me the dress code or the venue vibe.
User: I need a chic outfit for dinner. Response: That sounds like a fabulous evening! Is this a romantic date or a casual night out with friends, and do you prefer a sleek dress or tailored separates?
User: What do you think about the current election? Response: I prefer to stick to the polls of fashion rather than politics! Tell me, what kind of style aesthetic are we aiming for today?
"""
SUMMARY_PROMPT = """ SUMMARY_PROMPT = """
You are an expert fashion request analyzer. Analyze the conversation history provided by the user. You are an expert fashion request analyzer. Analyze the conversation history provided by the user.
Your task is to: Your task is to:
@@ -31,24 +64,32 @@ Your task is to:
Extract this information accurately from the chat history. Extract this information accurately from the chat history.
""" """
from app.taxonomy import FASHION_TAXONOMY, IGNORE_SUBCATEGORY, ALL_SUBCATEGORY_LIST from app.taxonomy import FASHION_TAXONOMY, IGNORE_SUBCATEGORY, ALL_SUBCATEGORY_LIST
core_outfit_template = f""" core_outfit_template = f"""
You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to recommend items for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Stylist Guide. You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to recommend items for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Stylist Guide.
Your task is to **create a cohesive and complete outfit**, strictly adhering to **BOTH** the user's explicit **Request Summary** and the **Outfit Style Guide**. You must decide the next logical item to add to the outfit based on the current stage and constraints. Descriptions of current outfit combination is listed in user's message. Your task is to **create a cohesive and complete outfit**, strictly adhering to **BOTH** the user's explicit **Request Summary** and the **Outfit Style Guide**. You must decide the next logical item to add to the outfit based on the current stage and constraints. Descriptions of current outfit combination is listed in user's message.
## RULE HIERARCHY (Priority Order):
1. **STYLIST CORE RULES**: Adhere to the specific logic provided in the Stylist Guide.
2. **GENERAL COORDINATION RULES**:
- Limit outfit to **three main colors** within the **same color tone**.
- Preferred composition: **Two neutrals and one accent color**.
- **Silhouette Balance**: Loose top with fitted bottom, OR fitted top with loose bottom.
- **Pants Logic**: Strictly NO mid-calf length pants. Cropped pants MUST be paired with loafers or ankle boots.
--- ---
## Request from the User: ## Request from the User:
{{request_summary}} {{request_summary}}
## Core Guidance Document: Outfit Style Guide ## Core Guidance Document: Stylist Guide
{{stylist_guide}} {{stylist_guide}}
--- ---
## Your Workflow and Constraints ## Your Workflow and Constraints
1. **Style Adherence**: You must strictly observe all rules in the Style Guide concerning **color palette, fit, layering principles, pattern restrictions , shoe coordination**. 1. **Style Adherence**: You must follow the Rule Hierarchy. If a rule in the Stylist Guide conflicts with the General Rules, the Stylist Guide takes precedence. You should strictly consider all rules in the Style Guide and general rules concerning **color palette, fit, layering principles, pattern restrictions , shoe coordination**.
2. **Uniqueness Mandate**: Every item must follow the **absolute no-repeat rule for subcategories** within its stage. Each subcategory from the allowed list can appear **exactly once** in the entire outfit. Furthermore, the categories 'dresses' and 'pants' and 'skirts' are mutually exclusive; they NORMALLY cannot be included in the same outfit. 2. **Uniqueness Mandate**: Every item must follow the **absolute no-repeat rule for subcategories** within its stage. Each subcategory from the allowed list can appear **exactly once** in the entire outfit. Furthermore, the categories 'dresses' and 'pants' and 'skirts' are mutually exclusive; they NORMALLY cannot be included in the same outfit.
3. **Step Planning**: The styling sequence must follow a logical approach (e.g., top-down, inside-out for clothing). Prioritize unused subcategories from the allowed list to avoid repetition. 3. **Step Planning**: The styling sequence must follow a logical approach (e.g., top-down, inside-out for clothing). Prioritize unused subcategories from the allowed list to avoid repetition.
4. **Structured Output**: Your output MUST be a valid JSON object. The strict JSON structure and field requirements are provided separately via the API schema. 4. **Structured Output**: Your output MUST be a valid JSON object. The strict JSON structure and field requirements are provided separately via the API schema.
@@ -88,17 +129,27 @@ You must strictly use the **JSON format** for your output, as follows:
Now, please start building an outfit (with strictly unique categories for all items) and output the JSON for the first item. Now, please start building an outfit (with strictly unique categories for all items) and output the JSON for the first item.
""" """
accessories_template = f""" accessories_template = f"""
You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to finalize the look by recommending accessories for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Accessories Guide. You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to finalize the look by recommending accessories for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Accessories Guide.
Your final task is to **select the perfect set of accessories** to complete the given outfit. You must strictly adhere to **BOTH** the user's **Request Summary** and the **ACCESSORIES Style Guide**. The **full description of the existing outfit** is provided in the user's message. Your final task is to **select the perfect set of accessories** to complete the given outfit. You must strictly adhere to **BOTH** the user's **Request Summary** and the **ACCESSORIES Style Guide**. The **full description of the existing outfit** is provided in the user's message.
## RULE HIERARCHY (Priority Order):
1. **ABSOLUTE PROHIBITION (Highest Priority)**:
- **NO WATCHES**.
- **NO HATS**.
- **NO SOCKS**.
- Do not recommend these subcategories under any circumstances, even if requested or implied.
2. **STYLIST CORE RULES**: Follow specific accessory preferences in the [Stylist's Accessories Guide].
3. **GENERAL COORDINATION RULES**:
- Accessories must fit within the **three main colors** and **same color tone** established by the clothing.
- Maintain the **two neutrals and one accent color** balance.
--- ---
## CONTEXT ## CONTEXT
[User Request]: {{request_summary}} [User Request]: {{request_summary}}
[Accessories Style Guide]: [Stylist's Accessories Guide]:
{{stylist_guide}} {{stylist_guide}}
--- ---
@@ -120,6 +171,15 @@ You are a professional fashion stylist Agent, specialized in creating complete,
You must create a cohesive look that includes **Clothing, Shoes, Bags, and Accessories**. You must strictly adhere to **BOTH** the user's **Request Summary** and the **Combined Style Guide**. You must create a cohesive look that includes **Clothing, Shoes, Bags, and Accessories**. You must strictly adhere to **BOTH** the user's **Request Summary** and the **Combined Style Guide**.
## RULE HIERARCHY (Priority Order):
1. **ABSOLUTE PROHIBITION**: Strictly NO hats or watches or socks.
2. **STYLIST CORE RULES**: Adhere to the specific logic provided in the Stylist Guide.
3. **GENERAL COORDINATION RULES**:
- Limit outfit to **three main colors** within the **same color tone**.
- Preferred composition: **Two neutrals and one accent color**.
- **Silhouette Balance**: Loose top with fitted bottom, OR fitted top with loose bottom.
- **Pants Logic**: Strictly NO mid-calf length pants. Cropped pants MUST be paired with loafers or ankle boots.
--- ---
## Request from the User: ## Request from the User:
{{request_summary}} {{request_summary}}
@@ -130,29 +190,28 @@ You must create a cohesive look that includes **Clothing, Shoes, Bags, and Acces
## GENERATION WORKFLOW & RULES ## GENERATION WORKFLOW & RULES
1. **Holistic Styling**: You are NOT recommending items step-by-step. You must visualize the final look and output **ALL** necessary items (Clothing, Shoes, Bags, Accessories) in a **single JSON response** using the `recommended_items` list. 1. **Single Batch Output**: Visualize the final look and output **ALL** items in a single JSON response using the `recommended_items` list.
2. **Outfit Composition Rules (Mandatory)**: 2. **Full Body Coverage & Composition (Mandatory)**:
* **CLOTHING**: Ensure **full body coverage**. You must include either [Top + Bottom] OR [One-piece (e.g., Dress/Jumpsuit)]. 'Dresses' and 'Skirts/Pants' are mutually exclusive. * **Clothing**: You MUST ensure **full body coverage**. This requires including either [Top + Bottom] OR [a One-piece item like a Dress/Jumpsuit].
* **SHOES**: **Exactly one (1) pair** of shoes is MANDATORY. * **Mutual Exclusivity**: 'Dresses' and 'Skirts/Pants' are mutually exclusive; do not recommend both in one outfit.
* **BAGS**: Recommend **0 or 1 bag**. Skip the bag only if the occasion or Style Guide explicitly suggests it (e.g., home wear, yoga). * **Shoes**: Exactly **one (1) pair** of shoes is MANDATORY for every outfit.
* **ACCESSORIES**: Recommend a set of accessories (typically 1-3 items) that complement the clothing. Follow metal/material constraints in the guide. * **Bags**: Recommend **0 or 1 bag**.
Number of items in outfit must not exceed {{max_len}}. * **Accessories**: Recommend a set of accessories (e.g., jewelry, eyewear).
3. **Uniqueness Mandate**: 3. **Strict Uniqueness Mandate (No Repeats)**:
* Each **subcategory** belonging to CLOTHING (e.g., 't-shirts', 'sweaters', 'jacket') can appear **EXACTLY ONCE** in the entire list. * Each **subcategory** (e.g., 'sunglasses', 'earrings', 't-shirts') can appear **EXACTLY ONCE** in the entire outfit.
* But **subcategory** belonging to ACCESSORIES can repeat. * DO NOT recommend two items from the same subcategory (e.g., do not recommend two different necklaces or two pairs of sunglasses).
4. **Exclusion List**: 4. **Style Adherence**:
* The following subcategories are **STRICTLY FORBIDDEN**: ({IGNORE_SUBCATEGORY}). Do not include them in your recommendation. - **Proportions**: Explicitly describe the fit (Loose vs. Fitted) to show the required silhouette balance.
- **Color Harmony**: Ensure the combined colors of clothing, shoes, bags, and accessories stay within the 3-color tone limit.
5. **Style Adherence**: 5. **Exclusion List**:
* Ensure all items coordinate in **color, fit, and material**. - The following subcategories are **STRICTLY FORBIDDEN**: ({IGNORE_SUBCATEGORY}).
* Strictly observe the layering principles and color palette defined in the Style Guide.
6. **Description Quality**: 6. **Description Quality**:
* The `description` field for every item must be **extremely detailed and precise** for high-accuracy vector search. - Each `description` MUST include: **Color, Fit/Silhouette, Material/Detail, and Role in the Outfit.**
* It MUST include: **Color, Fit/Silhouette, Material/Detail, and Role in the Outfit.**
## OUTPUT FORMAT ## OUTPUT FORMAT
Output a valid JSON object matching the provided API schema. The `recommended_items` array must contain all the items for this outfit. Output a valid JSON object matching the provided API schema. The `recommended_items` array must contain all the items for this outfit.
@@ -182,8 +241,8 @@ def build_iterative_schema(current_category):
return schema return schema
def build_batch_schema(specified_category: str=""): def build_batch_schema(specified_category: str = ""):
assert(specified_category in FASHION_TAXONOMY.keys() or specified_category == "") assert (specified_category in FASHION_TAXONOMY.keys() or specified_category == "")
if not specified_category: if not specified_category:
category_range_desc = "the complete final outfit (including all categories)" category_range_desc = "the complete final outfit (including all categories)"
subcategory_list = ALL_SUBCATEGORY_LIST subcategory_list = ALL_SUBCATEGORY_LIST

View File

@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class AsyncStylistAgent: class AsyncStylistAgent:
def __init__(self, local_db: str, gemini_model_name: str, outfit_id: str, stylist_name: str, gender: str): def __init__(self, local_db: str, gemini_model_name: str, outfit_id: str, stylist_name: str, gender: str, callback_url: str):
# self.outfit_items: List[Dict[str, str]] = [] # self.outfit_items: List[Dict[str, str]] = []
self.outfit_id = outfit_id self.outfit_id = outfit_id
self.stylist_name = stylist_name self.stylist_name = stylist_name
@@ -38,13 +38,6 @@ class AsyncStylistAgent:
self.local_db = local_db self.local_db = local_db
self.gemini_model_name = gemini_model_name self.gemini_model_name = gemini_model_name
self.stop_reason = "" self.stop_reason = ""
self.headers = {
'Accept': "*/*",
'Accept-Encoding': "gzip, deflate, br",
'User-Agent': "PostmanRuntime-ApipostRuntime/1.1.0",
'Connection': "keep-alive",
'Content-Type': "application/json"
}
# 存储桶配置 # 存储桶配置
try: try:
@@ -59,6 +52,7 @@ class AsyncStylistAgent:
) )
self.gcs_bucket = "lc_stylist_agent_outfit_items" self.gcs_bucket = "lc_stylist_agent_outfit_items"
self.minio_bucket = "lanecarford" self.minio_bucket = "lanecarford"
self.callback_url = f'{callback_url}/api/style/callback'
def _load_style_guide(self, stylist_name: str): def _load_style_guide(self, stylist_name: str):
"""加载 markdown 风格指南内容。""" """加载 markdown 风格指南内容。"""
@@ -178,6 +172,7 @@ class AsyncStylistAgent:
results = self.local_db.get_matched_item( results = self.local_db.get_matched_item(
query_embedding, query_embedding,
category, category,
subcategory,
occasions=occasions, occasions=occasions,
batch_sources=batch_sources, batch_sources=batch_sources,
gender=gender, gender=gender,
@@ -188,8 +183,13 @@ class AsyncStylistAgent:
results = [] results = []
if not results: if not results:
print(f"数据库中未找到符合 '{category}' 和描述的单品。") self.post_operation(
return None status="failed",
message=f"数据库中未找到符合 '{category}' 和描述的单品。",
callback_url=self.callback_url,
img_path="",
)
raise Exception(f"数据库中未找到符合 '{category}' 和描述的单品。")
# 3. 模拟 Agent 审核(实际应用中,你需要将图片发回给 Agent进行审核) # 3. 模拟 Agent 审核(实际应用中,你需要将图片发回给 Agent进行审核)
best_meta = results[0] # 第一个 batch 的第一个 metadata best_meta = results[0] # 第一个 batch 的第一个 metadata
@@ -200,6 +200,7 @@ class AsyncStylistAgent:
"category": best_meta['category'], "category": best_meta['category'],
'description': best_meta['description'], 'description': best_meta['description'],
"subcategory": best_meta['subcategory'], "subcategory": best_meta['subcategory'],
"brand": best_meta['brand'],
"gpt_description": item_description, "gpt_description": item_description,
"gpt_subcategory": subcategory, "gpt_subcategory": subcategory,
# 假设 'item_path' 存储在 metadata 中,或从 'item_id' 推导 # 假设 'item_path' 存储在 metadata 中,或从 'item_id' 推导
@@ -239,7 +240,7 @@ class AsyncStylistAgent:
context += "\nRecommend a **complete, full outfit**, including all items (clothing, shoes, bags, and accessories), strictly following the Request Summary and Style Guide. Output the **complete list** of items in a single JSON response." context += "\nRecommend a **complete, full outfit**, including all items (clothing, shoes, bags, and accessories), strictly following the Request Summary and Style Guide. Output the **complete list** of items in a single JSON response."
return context return context
def post_operation(self, status: str, message: str, callback_url: str, img_path: str): def post_operation(self, status: str, message: str, callback_url: str, img_path: str, request_summary=None, occasions=None):
"""处理完成后的回调操作。""" """处理完成后的回调操作。"""
if settings.LOCAL == 0: if settings.LOCAL == 0:
# 生产回调请求数据处理 # 生产回调请求数据处理
@@ -256,9 +257,17 @@ class AsyncStylistAgent:
'status': status, 'status': status,
# 'message': message, # 'message': message,
'path': img_path, 'path': img_path,
'outfit_id': self.outfit_id 'outfit_id': self.outfit_id,
"request_summary": request_summary,
"occasions": occasions
} }
response = post_request(url=callback_url, data=json.dumps(response_data), headers=self.headers)
if status in ['failed']:
# 失败直接打印参数 不发送结果
response_data['message'] = message
logger.info(f"request data {json.dumps(response_data, ensure_ascii=False, indent=2)}")
else:
response = post_request(url=callback_url, data=json.dumps(response_data))
logger.info(f"request data {json.dumps(response_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}") logger.info(f"request data {json.dumps(response_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}")
return response_data return response_data
else: else:
@@ -298,14 +307,13 @@ class AsyncStylistAgent:
gemini_data = self._parse_gemini_response(gemini_response_text) gemini_data = self._parse_gemini_response(gemini_response_text)
if not gemini_data: if not gemini_data:
print("Agent 返回无效响应,终止流程。")
self.post_operation( self.post_operation(
status="failed", status="failed",
message="Agent returned invalid response, terminating process.", message="Agent returned invalid response, terminating process.",
callback_url=url, callback_url=url,
img_path=merged_image_path, img_path=merged_image_path,
) )
break raise Exception("Agent 返回无效响应,终止流程。")
# 处理推荐单品 # 处理推荐单品
if gemini_data.get('action') == 'recommend_item': if gemini_data.get('action') == 'recommend_item':
@@ -385,15 +393,17 @@ class AsyncStylistAgent:
gemini_data = self._parse_gemini_response(gemini_response_text) gemini_data = self._parse_gemini_response(gemini_response_text)
recommended_items = gemini_data.get('recommended_items', []) recommended_items = gemini_data.get('recommended_items', [])
reason = gemini_data.get('reason', '') reason = gemini_data.get('reason', '')
failed_found_item_count = 0
if not recommended_items or not isinstance(recommended_items, List): if not recommended_items or not isinstance(recommended_items, List):
print("No recommended item from Gemini, terminating process.")
self.post_operation( self.post_operation(
status="failed", status="failed",
message="Agent returned invalid response, terminating process.", message="Agent returned invalid response, terminating process.",
callback_url=url, callback_url=url,
img_path=merged_image_path img_path=merged_image_path
) )
raise Exception("No recommended item from Gemini, terminating process.")
else: else:
for idx, rec_item in enumerate(recommended_items): for idx, rec_item in enumerate(recommended_items):
subcategory = rec_item.get('subcategory') subcategory = rec_item.get('subcategory')
@@ -416,10 +426,21 @@ class AsyncStylistAgent:
new_item = self._get_next_item(description, category, subcategory, occasions, batch_sources, self.gender) new_item = self._get_next_item(description, category, subcategory, occasions, batch_sources, self.gender)
if not new_item or new_item['item_id'] in [x['item_id'] for x in self.outfit_items]: if not new_item or new_item['item_id'] in [x['item_id'] for x in self.outfit_items]:
failed_found_item_count += 1
continue continue
else: else:
self.outfit_items.append(new_item) self.outfit_items.append(new_item)
print(f"Item {idx + 1}: ({subcategory}) {rec_item}, found item: {new_item}") print(f"Item {idx + 1}: ({subcategory}) {rec_item}, found item: {new_item}")
# 如果没有找到的item过于多需要重试
if failed_found_item_count / len(recommended_items) > 0.5:
self.post_operation(
status="failed",
message=f"There are {failed_found_item_count} items (total {len(recommended_items)}) are not found in the database",
callback_url=url,
img_path=merged_image_path
)
raise Exception(f"There are {failed_found_item_count} items (total {len(recommended_items)}) are not found in the database")
return reason return reason
async def run_iterative_styling(self, request_summary, occasions, start_outfit: Optional[List] = None, batch_sources: List = [], user_id="test", callback_url=""): async def run_iterative_styling(self, request_summary, occasions, start_outfit: Optional[List] = None, batch_sources: List = [], user_id="test", callback_url=""):
@@ -464,18 +485,20 @@ class AsyncStylistAgent:
) )
final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name) final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name)
# 推荐完成返回
response_data = self.post_operation( response_data = self.post_operation(
status="stop", status="stop",
message=reason, message=reason,
callback_url=url, callback_url=url,
img_path=final_image_path img_path=final_image_path,
request_summary=request_summary,
occasions=occasions
) )
if settings.LOCAL == 1:
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items}, f, indent=2)
end_time = time.monotonic() end_time = time.monotonic()
total_duration = end_time - start_time total_duration = end_time - start_time
if settings.LOCAL == 1:
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items, "total_duration": total_duration}, f, indent=2)
return response_data, total_duration return response_data, total_duration
@@ -502,20 +525,27 @@ class AsyncStylistAgent:
user_id, user_id,
url url
) )
# 推荐即将完成 回调通知前端
self.post_operation(
status="almost_done",
message="Recommendation has been completed and the outfit is being assembled",
callback_url=url,
img_path="",
)
final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name) final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name)
response_data = self.post_operation( response_data = self.post_operation(
status="stop", status="stop",
message=reason, message=reason,
callback_url=url, callback_url=url,
img_path=final_image_path img_path=final_image_path,
request_summary=request_summary,
occasions=occasions
) )
if settings.LOCAL == 1:
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items}, f, indent=2)
end_time = time.monotonic() end_time = time.monotonic()
total_duration = end_time - start_time total_duration = end_time - start_time
if settings.LOCAL == 1:
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items, "total_duration": total_duration}, f, indent=2)
return response_data, total_duration return response_data, total_duration

View File

@@ -5,7 +5,7 @@ from PIL import Image
from typing import List, Dict, Any from typing import List, Dict, Any
from transformers import CLIPProcessor, CLIPModel from transformers import CLIPProcessor, CLIPModel
from app.taxonomy import OCCASION, CATEGORY_LIST, IGNORE_SUBCATEGORY from app.taxonomy import OCCASION, CATEGORY_LIST, IGNORE_SUBCATEGORY, BRAND_WHITELIST
class VectorDatabase(): class VectorDatabase():
@@ -46,7 +46,7 @@ class VectorDatabase():
return features.cpu().numpy().flatten().tolist() return features.cpu().numpy().flatten().tolist()
def get_matched_item(self, embedding: List[float], category: str, occasions: List[str] = [], batch_sources: List[str] = [], gender: str = 'female', n_results: int = 1) -> List[Dict[str, Any]]: def get_matched_item(self, embedding: List[float], category: str, subcategory: str, occasions: List[str] = [], batch_sources: List[str] = [], gender: str = 'female', n_results: int = 1) -> List[Dict[str, Any]]:
if category not in CATEGORY_LIST: if category not in CATEGORY_LIST:
raise ValueError(f"Recommended {category} is not valid.") raise ValueError(f"Recommended {category} is not valid.")
@@ -59,6 +59,12 @@ class VectorDatabase():
]}, ]},
{"subcategory": {"$nin": IGNORE_SUBCATEGORY}} {"subcategory": {"$nin": IGNORE_SUBCATEGORY}}
] ]
# 加了一条限制但是部署到生产的时候把他设定为False
brand_strication = False
if brand_strication:
and_conditions.append({"brand": {"$in": BRAND_WHITELIST}})
if batch_sources and len(batch_sources) > 0: if batch_sources and len(batch_sources) > 0:
if len(batch_sources) == 1: if len(batch_sources) == 1:
and_conditions.append({"batch_source": batch_sources[0]}) and_conditions.append({"batch_source": batch_sources[0]})
@@ -80,10 +86,14 @@ class VectorDatabase():
return [] return []
metadatas = results['metadatas'][0] # List[Dict[str, Any]] metadatas = results['metadatas'][0] # List[Dict[str, Any]]
final_scores = [] candidate_pool = []
all_scores = []
for idx, metadata in enumerate(metadatas): for idx, metadata in enumerate(metadatas):
dist_img = results['distances'][0][idx] dist_img = results['distances'][0][idx]
score_vec = 1 - dist_img # cosine similarity range: [-1, 1] score_vec = 1 - dist_img # cosine similarity range: [-1, 1]
score_subcategory = 0.0
if subcategory == metadata['subcategory']:
score_subcategory = 1
score_occ = 0.0 score_occ = 0.0
if occasions: if occasions:
@@ -94,26 +104,42 @@ class VectorDatabase():
count += 1 count += 1
status_val = metadata.get(occ, -1) status_val = metadata.get(occ, -1)
if status_val == 1: if status_val == 1:
score_occ += 1.0 score_occ += 5.0
elif status_val == 0: elif status_val == 0:
score_occ += 0.0 score_occ += 0.0
else: else:
score_occ -= 100.0 score_occ -= 5.0
score_occ = score_occ / count if count else 0.0 score_occ = score_occ / count if count else 0.0
final_score = 0.6 * score_vec + 0.4 * score_occ final_score = 0.5 * score_vec + 0.1 * score_occ + 0.5 * score_subcategory
final_scores.append(final_score) all_scores.append(final_score)
if final_score > 0:
candidate_pool.append({
"score": final_score,
"metadata": metadata
})
scores_arr = np.array(final_scores) if not candidate_pool:
temperature = 0.5 print(f"All scores are negative: Ignore")
scores_arr = scores_arr / temperature return []
# 采取topk截断
candidate_pool.sort(key=lambda x: x['score'], reverse=True)
current_k = min(10, len(candidate_pool))
top_candidates = candidate_pool[:current_k]
top_scores = np.array([x['score'] for x in top_candidates])
#降低温度,使得选择稳定
temperature = 0.2
scaled_scores = top_scores / temperature
# Softmax: 将分数转换为概率 # Softmax: 将分数转换为概率
exp_scores = np.exp(scores_arr - np.max(scores_arr)) exp_scores = np.exp(scaled_scores - np.max(scaled_scores))
probabilities = exp_scores / np.sum(exp_scores) probabilities = exp_scores / np.sum(exp_scores)
# 采样 (或直接取 Top 1) # 采样 (或直接取 Top 1)
sampled_index = np.random.choice(a=len(results['ids'][0]), p=probabilities, size=n_results, replace=False) # 不重复采样 sampled_index = np.random.choice(a=current_k, p=probabilities, size=min(n_results, current_k), replace=False) # 不重复采样
sampled_items = [metadatas[i] for i in sampled_index] sampled_items = [top_candidates[i]['metadata'] for i in sampled_index]
return sampled_items return sampled_items

View File

@@ -4,18 +4,24 @@ import time
import requests import requests
def post_request(url, data=None, json_data=None, headers=None, auth=None, timeout=5): def post_request(url, data=None, json_data=None, auth=None, timeout=5):
""" """
发送POST请求的封装函数 发送POST请求的封装函数
:param url: 接口的URL地址 :param url: 接口的URL地址
:param data: 要发送的数据(字典形式,用于表单数据等,会自动编码) :param data: 要发送的数据(字典形式,用于表单数据等,会自动编码)
:param json_data: 要发送的JSON数据字典形式会自动转换为JSON字符串 :param json_data: 要发送的JSON数据字典形式会自动转换为JSON字符串
:param headers: 请求头字典
:param auth: 认证信息(如 ('username', 'password') 形式用于基本认证) :param auth: 认证信息(如 ('username', 'password') 形式用于基本认证)
:param timeout: 超时时间,单位为秒 :param timeout: 超时时间,单位为秒
:return: 返回接口的响应对象 :return: 返回接口的响应对象
""" """
headers = {
'Accept': "*/*",
'Accept-Encoding': "gzip, deflate, br",
'User-Agent': "PostmanRuntime-ApipostRuntime/1.1.0",
'Connection': "keep-alive",
'Content-Type': "application/json"
}
try: try:
response = requests.post( response = requests.post(
url, url,
@@ -52,6 +58,6 @@ if __name__ == '__main__':
'Content-Type': "application/json" 'Content-Type': "application/json"
} }
start_time = time.time() start_time = time.time()
X = post_request(url=url, data=json.dumps(object_data), headers=headers) X = post_request(url=url, data=json.dumps(object_data))
print(time.time() - start_time) print(time.time() - start_time)
print(X) print(X)

View File

@@ -91,4 +91,43 @@ FASHION_TAXONOMY = {
CATEGORY_LIST = list(FASHION_TAXONOMY.keys()) CATEGORY_LIST = list(FASHION_TAXONOMY.keys())
ALL_SUBCATEGORY_LIST = sum(FASHION_TAXONOMY.values(), []) ALL_SUBCATEGORY_LIST = sum(FASHION_TAXONOMY.values(), [])
IGNORE_SUBCATEGORY = ['socks'] IGNORE_SUBCATEGORY = ['socks', 'watches', 'hats']
BRAND_WHITELIST = [
"ALAÏA",
"BALENCIAGA",
"BALMAIN",
"BARBOUR",
"BOTTEGA VENETA",
"BRUNELLO CUCINELLI",
"ALEXANDERWANG",
"CHLOÉ",
"DIOR",
"FEAR OF GOD",
"FENDI",
"GIA STUDIOS",
"GUCCI",
"HELMUT LANG",
"JACQUEMUS",
"JIMMY CHOO",
"JW ANDERSON",
"KHAITE",
"LEMAIRE",
"LOEWE",
"MAISON MARGIELA",
"MIU MIU",
"MM6 MAISON MARGIELA",
"MONCLER",
"PEDDER RED",
"PETER DO",
"PHOEBE PHILO",
"PRADA",
"RICK OWENS",
"SACAI",
"SKIMS",
"THE ROW",
"THEORY",
"THOM BROWNE",
"THE FRANKIE SHOP",
"TOTEME",
]

22
pyproject.toml Executable file
View File

@@ -0,0 +1,22 @@
[project]
name = "lc-stylist-agent"
version = "0.1.0"
description = "Add your description here"
requires-python = "==3.10.*"
dependencies = [
"chromadb==1.1.1",
"google-cloud-storage==2.19.0",
"google-genai==1.45.0",
"litserve>=0.2.16",
"minio==7.2.18",
"numpy==1.24.4",
"open-clip-torch==2.24.0",
"opencv-python==4.9.0.80",
"pydantic-settings==2.11.0",
"pytorch-fid==0.3.0",
"redis==6.4.0",
"setuptools==80.9.0",
"torch-fidelity==0.3.0",
"torchmetrics==1.4.0.post0",
"transformers==4.41.1",
]

2134
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff