Compare commits
13 Commits
develop
...
507d8a3e12
| Author | SHA1 | Date | |
|---|---|---|---|
| 507d8a3e12 | |||
| feb431e9c1 | |||
| 042e6015f0 | |||
| 8ccf899441 | |||
| 7a1496aeb7 | |||
| d8df6b28ea | |||
| 79ec1c5300 | |||
| 1f6ba5fd09 | |||
|
|
54aac900ad | ||
| 884e7966dd | |||
| 2db36e2c1d | |||
|
|
3910c07c40 | ||
|
|
c0c72a9c87 |
40
.gitea/workflows/prod_build_manual.yaml
Normal file
40
.gitea/workflows/prod_build_manual.yaml
Normal 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
2
.gitignore
vendored
@@ -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
|
||||||
63
Dockerfile
63
Dockerfile
@@ -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"]
|
|
||||||
|
|||||||
@@ -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表示生产环境运行")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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查询对话历史,总结对话内容
|
||||||
request_summary, occasions = await self.get_conversation_summary(request.session_id)
|
if request.request_summary and request.occasions:
|
||||||
logger.info(f"request_summary: {request_summary}")
|
request_summary = request.request_summary
|
||||||
|
occasions = request.occasions
|
||||||
|
else:
|
||||||
|
request_summary, occasions = await self.get_conversation_summary(request.session_id)
|
||||||
|
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,52 +195,45 @@ 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:
|
||||||
|
tasks_mapping[outfit_ids[0]] = "fast"
|
||||||
|
else:
|
||||||
|
for i in range(num_outfits):
|
||||||
|
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刷新
|
# 通过请求数量判断 num == 1 单个outfit刷新
|
||||||
stylist_agent_kwages['outfit_id'] = outfit_ids[0]
|
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)
|
||||||
task = agent.run_iterative_styling(
|
if v == "fast":
|
||||||
request_summary=request_summary,
|
task = agent.run_quick_batch_styling(
|
||||||
occasions=occasions,
|
request_summary=request_summary,
|
||||||
start_outfit=start_outfit,
|
occasions=occasions,
|
||||||
batch_sources=batch_sources,
|
start_outfit=start_outfit,
|
||||||
user_id=user_id,
|
batch_sources=batch_sources,
|
||||||
callback_url=callback_url,
|
user_id=user_id,
|
||||||
)
|
callback_url=callback_url,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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)
|
tasks.append(task)
|
||||||
task_map[task] = {"outfit_id": outfit_ids[0], "retries": 0}
|
task_map[task] = {"outfit_id": k, "retries": 0}
|
||||||
elif num_outfits > 1:
|
|
||||||
# 通过请求数量判断 num > 1 四套搭配推荐 (1快 , num-1慢)
|
|
||||||
for i in range(num_outfits):
|
|
||||||
stylist_agent_kwages['outfit_id'] = outfit_ids[i]
|
|
||||||
stylist_agent_kwages['stylist_name'] = stylist_name
|
|
||||||
stylist_agent_kwages['gender'] = gender
|
|
||||||
agent = AsyncStylistAgent(**stylist_agent_kwages)
|
|
||||||
if i == 0:
|
|
||||||
# 第一套搭配使用快速方法 一次跑出所有单品
|
|
||||||
logger.info(f"fast request outfit_id is : {outfit_ids[i]}")
|
|
||||||
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:
|
|
||||||
# 后续
|
|
||||||
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[i], "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,15 +272,27 @@ 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)
|
||||||
new_task = agent.run_quick_batch_styling(
|
if tasks_mapping[outfit_id] == "fast":
|
||||||
request_summary=request_summary,
|
new_task = agent.run_quick_batch_styling(
|
||||||
occasions=occasions,
|
request_summary=request_summary,
|
||||||
start_outfit=start_outfit,
|
occasions=occasions,
|
||||||
batch_sources=batch_sources,
|
start_outfit=start_outfit,
|
||||||
user_id=user_id,
|
batch_sources=batch_sources,
|
||||||
callback_url=callback_url
|
user_id=user_id,
|
||||||
)
|
callback_url=callback_url,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_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
|
||||||
|
)
|
||||||
# 将新任务添加到下一轮运行列表,并更新任务映射
|
# 将新任务添加到下一轮运行列表,并更新任务映射
|
||||||
next_tasks_to_run.append(new_task)
|
next_tasks_to_run.append(new_task)
|
||||||
task_map[new_task] = task_info # 新任务继承旧任务的重试信息
|
task_map[new_task] = task_info # 新任务继承旧任务的重试信息
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}")
|
'I’m 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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,10 +257,18 @@ 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)
|
|
||||||
logger.info(f"request data :{json.dumps(response_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}")
|
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}")
|
||||||
return response_data
|
return response_data
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
22
pyproject.toml
Executable 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",
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user