service-forge 0.1.11__py3-none-any.whl → 0.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of service-forge might be problematic. Click here for more details.

Files changed (35) hide show
  1. service_forge/api/http_api.py +4 -0
  2. service_forge/api/routers/feedback/feedback_router.py +148 -0
  3. service_forge/api/routers/service/service_router.py +22 -32
  4. service_forge/current_service.py +14 -0
  5. service_forge/db/database.py +29 -32
  6. service_forge/db/migrations/feedback_migration.py +154 -0
  7. service_forge/db/models/__init__.py +0 -0
  8. service_forge/db/models/feedback.py +33 -0
  9. service_forge/llm/__init__.py +5 -0
  10. service_forge/model/feedback.py +30 -0
  11. service_forge/service.py +118 -126
  12. service_forge/service_config.py +42 -156
  13. service_forge/sft/config/injector.py +33 -23
  14. service_forge/sft/config/sft_config.py +55 -8
  15. service_forge/storage/__init__.py +5 -0
  16. service_forge/storage/feedback_storage.py +245 -0
  17. service_forge/utils/workflow_clone.py +3 -2
  18. service_forge/workflow/node.py +8 -0
  19. service_forge/workflow/nodes/llm/query_llm_node.py +1 -1
  20. service_forge/workflow/trigger.py +4 -0
  21. service_forge/workflow/triggers/a2a_api_trigger.py +2 -0
  22. service_forge/workflow/triggers/fast_api_trigger.py +32 -0
  23. service_forge/workflow/triggers/kafka_api_trigger.py +3 -0
  24. service_forge/workflow/triggers/once_trigger.py +4 -1
  25. service_forge/workflow/triggers/period_trigger.py +4 -1
  26. service_forge/workflow/triggers/websocket_api_trigger.py +15 -11
  27. service_forge/workflow/workflow.py +26 -4
  28. service_forge/workflow/workflow_config.py +66 -0
  29. service_forge/workflow/workflow_factory.py +86 -85
  30. service_forge/workflow/workflow_group.py +33 -9
  31. {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/METADATA +1 -1
  32. {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/RECORD +34 -26
  33. service_forge/api/routers/service/__init__.py +0 -4
  34. {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/WHEEL +0 -0
  35. {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/entry_points.txt +0 -0
@@ -8,6 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
8
8
  from fastapi.openapi.utils import get_openapi
9
9
  from service_forge.api.routers.websocket.websocket_router import websocket_router
10
10
  from service_forge.api.routers.service.service_router import service_router
11
+ from service_forge.api.routers.feedback.feedback_router import router as feedback_router
11
12
  from service_forge.sft.config.sf_metadata import load_metadata
12
13
  from service_forge.sft.util.name_util import get_service_url_name
13
14
 
@@ -78,6 +79,9 @@ def create_app(
78
79
 
79
80
  # Always include WebSocket router
80
81
  app.include_router(websocket_router)
82
+
83
+ # Include Feedback router
84
+ app.include_router(feedback_router)
81
85
 
82
86
  # Always include Service router
83
87
  app.include_router(service_router)
@@ -0,0 +1,148 @@
1
+ import os
2
+ import httpx
3
+ from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
4
+ from loguru import logger
5
+ from typing import Optional
6
+
7
+ from service_forge.model.feedback import FeedbackCreate, FeedbackResponse, FeedbackListResponse
8
+ from service_forge.storage.feedback_storage import feedback_storage
9
+ from service_forge.db.database import create_database_manager
10
+ from service_forge.current_service import get_service
11
+
12
+ router = APIRouter(prefix="/sdk/feedback", tags=["feedback"])
13
+
14
+ def get_forward_api_url():
15
+ if not get_service():
16
+ return None
17
+ if not get_service().config.feedback:
18
+ return None
19
+ return get_service().config.feedback.api_url
20
+
21
+ def get_forward_api_timeout():
22
+ if not get_service():
23
+ return None
24
+ if not get_service().config.feedback:
25
+ return None
26
+ return get_service().config.feedback.api_timeout
27
+
28
+ async def forward_feedback_to_api(feedback_data: dict):
29
+ """
30
+ 将反馈数据转发到外部 API
31
+
32
+ Args:
33
+ feedback_data: 反馈数据字典
34
+ """
35
+ forward_api_url = get_forward_api_url()
36
+ forward_api_timeout = get_forward_api_timeout()
37
+ print(forward_api_url)
38
+ print(forward_api_timeout)
39
+ if not forward_api_url:
40
+ logger.debug("未配置转发 API URL,跳过转发")
41
+ return
42
+ try:
43
+ # 处理 datetime 对象,转换为 ISO 格式字符串
44
+ serializable_data = feedback_data.copy()
45
+ if 'created_at' in serializable_data and serializable_data['created_at']:
46
+ serializable_data['created_at'] = serializable_data['created_at'].isoformat()
47
+
48
+ async with httpx.AsyncClient(timeout=forward_api_timeout) as client:
49
+ response = await client.post(
50
+ forward_api_url,
51
+ json=serializable_data,
52
+ headers={"Content-Type": "application/json"}
53
+ )
54
+ response.raise_for_status()
55
+ logger.info(f"反馈转发成功: feedback_id={feedback_data.get('feedback_id')}, status={response.status_code}")
56
+ except httpx.TimeoutException:
57
+ logger.warning(f"反馈转发超时: {forward_api_url}")
58
+ except httpx.ConnectError as e:
59
+ logger.warning(f"反馈转发连接失败: {forward_api_url} - {e}")
60
+ except httpx.HTTPStatusError as e:
61
+ logger.error(f"反馈转发失败: status={e.response.status_code}, detail={e.response.text}")
62
+ except Exception as e:
63
+ logger.error(f"反馈转发异常: {type(e).__name__}: {e}")
64
+
65
+
66
+
67
+ @router.post("/", response_model=FeedbackResponse, summary="创建工作流反馈")
68
+ async def create_feedback(feedback: FeedbackCreate, background_tasks: BackgroundTasks):
69
+ """
70
+ 创建工作流执行完成后的用户反馈
71
+ - **task_id**: 工作流任务ID - workflow的id
72
+ - **workflow_name**: 工作流名称 - workflow的名称
73
+ - **rating**: 可选的评分 (1-5) - 反馈中的一种,评分,可以为空
74
+ - **comment**: 可选的用户评论 - 反馈中的一种,可以为空
75
+ - **metadata**: 可选的额外元数据
76
+ 还少什么? - trace_id?
77
+ """
78
+ try:
79
+ # 保存到数据库
80
+ feedback_data = await feedback_storage.create_feedback(
81
+ task_id=feedback.task_id,
82
+ workflow_name=feedback.workflow_name,
83
+ rating=feedback.rating,
84
+ comment=feedback.comment,
85
+ metadata=feedback.metadata,
86
+ )
87
+
88
+ # 后台任务转发到外部 API (不阻塞响应)
89
+ background_tasks.add_task(forward_feedback_to_api, feedback_data)
90
+
91
+ return FeedbackResponse(**feedback_data)
92
+ except Exception as e:
93
+ logger.error(f"创建反馈失败: {e}")
94
+ raise HTTPException(status_code=500, detail=f"创建反馈失败: {str(e)}")
95
+
96
+
97
+ @router.get("/{feedback_id}", response_model=FeedbackResponse, summary="获取单个反馈")
98
+ async def get_feedback(feedback_id: str):
99
+ """
100
+ 根据反馈ID获取反馈详情
101
+
102
+ - **feedback_id**: 反馈ID
103
+ """
104
+ feedback = await feedback_storage.get_feedback(feedback_id)
105
+ if not feedback:
106
+ raise HTTPException(status_code=404, detail="反馈不存在")
107
+ return FeedbackResponse(**feedback)
108
+
109
+
110
+ @router.get("/", response_model=FeedbackListResponse, summary="获取反馈列表")
111
+ async def list_feedbacks(
112
+ task_id: Optional[str] = Query(None, description="按任务ID筛选"),
113
+ workflow_name: Optional[str] = Query(None, description="按工作流名称筛选"),
114
+ ):
115
+ """
116
+ 获取反馈列表,支持按任务ID或工作流名称筛选
117
+
118
+ - **task_id**: 可选,按任务ID筛选
119
+ - **workflow_name**: 可选,按工作流名称筛选
120
+ """
121
+ try:
122
+ if task_id:
123
+ feedbacks = await feedback_storage.get_feedbacks_by_task(task_id)
124
+ elif workflow_name:
125
+ feedbacks = await feedback_storage.get_feedbacks_by_workflow(workflow_name)
126
+ else:
127
+ feedbacks = await feedback_storage.get_all_feedbacks()
128
+
129
+ return FeedbackListResponse(
130
+ total=len(feedbacks),
131
+ feedbacks=[FeedbackResponse(**f) for f in feedbacks]
132
+ )
133
+ except Exception as e:
134
+ logger.error(f"获取反馈列表失败: {e}")
135
+ raise HTTPException(status_code=500, detail=f"获取反馈列表失败: {str(e)}")
136
+
137
+
138
+ @router.delete("/{feedback_id}", summary="删除反馈")
139
+ async def delete_feedback(feedback_id: str):
140
+ """
141
+ 删除指定的反馈
142
+
143
+ - **feedback_id**: 反馈ID
144
+ """
145
+ success = await feedback_storage.delete_feedback(feedback_id)
146
+ if not success:
147
+ raise HTTPException(status_code=404, detail="反馈不存在")
148
+ return {"message": "反馈删除成功", "feedback_id": feedback_id}
@@ -1,24 +1,13 @@
1
+ import os
2
+ import uuid
3
+ import tempfile
1
4
  from fastapi import APIRouter, HTTPException, UploadFile, File, Form
2
5
  from fastapi.responses import JSONResponse
3
6
  from loguru import logger
4
7
  from typing import Optional, TYPE_CHECKING
5
- import tempfile
6
- import os
7
8
  from pydantic import BaseModel
8
9
  from omegaconf import OmegaConf
9
-
10
- if TYPE_CHECKING:
11
- from service_forge.service import Service
12
-
13
- # TODO: refactor this, do not use global variable
14
- _current_service: Optional['Service'] = None
15
-
16
- def set_service(service: 'Service') -> None:
17
- global _current_service
18
- _current_service = service
19
-
20
- def get_service() -> Optional['Service']:
21
- return _current_service
10
+ from service_forge.current_service import get_service
22
11
 
23
12
  service_router = APIRouter(prefix="/sdk/service", tags=["service"])
24
13
 
@@ -29,6 +18,7 @@ class WorkflowStatusResponse(BaseModel):
29
18
  workflows: list[dict]
30
19
 
31
20
  class WorkflowActionResponse(BaseModel):
21
+ workflow_id: str
32
22
  success: bool
33
23
  message: str
34
24
 
@@ -45,43 +35,42 @@ async def get_service_status():
45
35
  logger.error(f"Error getting service status: {e}")
46
36
  raise HTTPException(status_code=500, detail=str(e))
47
37
 
48
- @service_router.post("/workflow/{workflow_name}/start", response_model=WorkflowActionResponse)
49
- async def start_workflow(workflow_name: str):
38
+ @service_router.post("/workflow/{workflow_id}/start", response_model=WorkflowActionResponse)
39
+ async def start_workflow(workflow_id: str):
50
40
  service = get_service()
51
41
  if service is None:
52
42
  raise HTTPException(status_code=503, detail="Service not initialized")
53
43
 
54
44
  try:
55
- success = await service.start_workflow(workflow_name)
45
+ success = service.start_workflow_by_id(uuid.UUID(workflow_id))
56
46
  if success:
57
- return WorkflowActionResponse(success=True, message=f"Workflow {workflow_name} started successfully")
47
+ return WorkflowActionResponse(success=True, message=f"Workflow {workflow_id} started successfully")
58
48
  else:
59
- return WorkflowActionResponse(success=False, message=f"Failed to start workflow {workflow_name}")
49
+ return WorkflowActionResponse(success=False, message=f"Failed to start workflow {workflow_id}")
60
50
  except Exception as e:
61
- logger.error(f"Error starting workflow {workflow_name}: {e}")
51
+ logger.error(f"Error starting workflow {workflow_id}: {e}")
62
52
  raise HTTPException(status_code=500, detail=str(e))
63
53
 
64
- @service_router.post("/workflow/{workflow_name}/stop", response_model=WorkflowActionResponse)
65
- async def stop_workflow(workflow_name: str):
54
+ @service_router.post("/workflow/{workflow_id}/stop", response_model=WorkflowActionResponse)
55
+ async def stop_workflow(workflow_id: str):
66
56
  service = get_service()
67
57
  if service is None:
68
58
  raise HTTPException(status_code=503, detail="Service not initialized")
69
59
 
70
60
  try:
71
- success = await service.stop_workflow(workflow_name)
61
+ success = await service.stop_workflow_by_id(uuid.UUID(workflow_id))
72
62
  if success:
73
- return WorkflowActionResponse(success=True, message=f"Workflow {workflow_name} stopped successfully")
63
+ return WorkflowActionResponse(success=True, message=f"Workflow {workflow_id} stopped successfully")
74
64
  else:
75
- return WorkflowActionResponse(success=False, message=f"Failed to stop workflow {workflow_name}")
65
+ return WorkflowActionResponse(success=False, message=f"Failed to stop workflow {workflow_id}")
76
66
  except Exception as e:
77
- logger.error(f"Error stopping workflow {workflow_name}: {e}")
67
+ logger.error(f"Error stopping workflow {workflow_id}: {e}")
78
68
  raise HTTPException(status_code=500, detail=str(e))
79
69
 
80
70
  @service_router.post("/workflow/upload", response_model=WorkflowActionResponse)
81
71
  async def upload_workflow_config(
82
72
  file: Optional[UploadFile] = File(None),
83
73
  config_content: Optional[str] = Form(None),
84
- workflow_name: Optional[str] = Form(None)
85
74
  ):
86
75
  service = get_service()
87
76
  if service is None:
@@ -104,18 +93,19 @@ async def upload_workflow_config(
104
93
  content = await file.read()
105
94
  temp_file.write(content)
106
95
  temp_file_path = temp_file.name
107
-
108
- success = await service.load_workflow_from_config(config_path=temp_file_path, workflow_name=workflow_name)
96
+
97
+ workflow_id = await service.load_workflow_from_config(config_path=temp_file_path)
109
98
  else:
110
99
  try:
111
100
  config = OmegaConf.to_object(OmegaConf.create(config_content))
112
101
  except Exception as e:
113
102
  raise HTTPException(status_code=400, detail=f"Invalid YAML format: {str(e)}")
114
103
 
115
- success = await service.load_workflow_from_config(config=config, workflow_name=workflow_name)
104
+ workflow_id = await service.load_workflow_from_config(config=config)
116
105
 
117
- if success:
106
+ if workflow_id:
118
107
  return WorkflowActionResponse(
108
+ workflow_id=str(workflow_id),
119
109
  success=True,
120
110
  message=f"Workflow configuration uploaded and loaded successfully"
121
111
  )
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from service_forge.service import Service
6
+
7
+ _current_service: Service | None = None
8
+
9
+ def set_service(service: Service) -> None:
10
+ global _current_service
11
+ _current_service = service
12
+
13
+ def get_service() -> Service | None:
14
+ return _current_service
@@ -5,9 +5,8 @@ import pymongo
5
5
  import psycopg2
6
6
  from typing import AsyncGenerator
7
7
  from loguru import logger
8
- from omegaconf import OmegaConf
9
8
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
10
- from sqlalchemy import text
9
+ from service_forge.service_config import ServiceConfig
11
10
 
12
11
  class PostgresDatabase:
13
12
  def __init__(
@@ -188,50 +187,48 @@ class DatabaseManager:
188
187
  return None
189
188
 
190
189
  @staticmethod
191
- def from_config(config_path: str = None, config = None) -> DatabaseManager:
190
+ def from_config(config_path: str = None, config: ServiceConfig = None) -> DatabaseManager:
192
191
  if config is None:
193
- config = OmegaConf.to_object(OmegaConf.load(config_path))
192
+ config = ServiceConfig.from_yaml_file(config_path)
194
193
 
195
- databases_config = config.get('databases', None)
196
194
  postgres_databases = []
197
195
  mongo_databases = []
198
196
  redis_databases = []
197
+
198
+ databases_config = config.databases
199
+
199
200
  if databases_config is not None:
200
201
  for database_config in databases_config:
201
- if ('postgres_host' in database_config and database_config['postgres_host'] is not None) + \
202
- ('mongo_host' in database_config and database_config['mongo_host'] is not None) + \
203
- ('redis_host' in database_config and database_config['redis_host'] is not None) == 0:
204
- raise ValueError(f"Database '{database_config['name']}' is missing required configuration. Please check your service.yaml file.")
205
-
206
- if ('postgres_host' in database_config and database_config['postgres_host'] is not None) + \
207
- ('mongo_host' in database_config and database_config['mongo_host'] is not None) + \
208
- ('redis_host' in database_config and database_config['redis_host'] is not None) > 1:
202
+ if all([database_config.postgres_host is None, database_config.mongo_host is None, database_config.redis_host is None]):
203
+ raise ValueError(f"Database '{database_config.name}' is missing required configuration. Please check your service.yaml file.")
204
+
205
+ if (database_config.postgres_host is not None) + (database_config.mongo_host is not None) + (database_config.redis_host is not None) > 1:
209
206
  raise ValueError(f"Database '{database_config['name']}' has multiple host configurations. Please check your service.yaml file.")
210
207
 
211
- if 'postgres_host' in database_config and database_config['postgres_host'] is not None:
208
+ if database_config.postgres_host is not None:
212
209
  postgres_databases.append(PostgresDatabase(
213
- name=database_config['name'],
214
- postgres_user=database_config['postgres_user'],
215
- postgres_password=database_config['postgres_password'],
216
- postgres_host=database_config['postgres_host'],
217
- postgres_port=database_config['postgres_port'],
218
- postgres_db=database_config['postgres_db'],
210
+ name=database_config.name,
211
+ postgres_user=database_config.postgres_user,
212
+ postgres_password=database_config.postgres_password,
213
+ postgres_host=database_config.postgres_host,
214
+ postgres_port=database_config.postgres_port,
215
+ postgres_db=database_config.postgres_db,
219
216
  ))
220
- if 'mongo_host' in database_config and database_config['mongo_host'] is not None:
217
+ elif database_config.mongo_host is not None:
221
218
  mongo_databases.append(MongoDatabase(
222
- name=database_config['name'],
223
- mongo_host=database_config['mongo_host'],
224
- mongo_port=database_config['mongo_port'],
225
- mongo_user=database_config['mongo_user'],
226
- mongo_password=database_config['mongo_password'],
227
- mongo_db=database_config['mongo_db'],
219
+ name=database_config.name,
220
+ mongo_host=database_config.mongo_host,
221
+ mongo_port=database_config.mongo_port,
222
+ mongo_user=database_config.mongo_user,
223
+ mongo_password=database_config.mongo_password,
224
+ mongo_db=database_config.mongo_db,
228
225
  ))
229
- if 'redis_host' in database_config and database_config['redis_host'] is not None:
226
+ elif database_config.redis_host is not None:
230
227
  redis_databases.append(RedisDatabase(
231
- name=database_config['name'],
232
- redis_host=database_config['redis_host'],
233
- redis_port=database_config['redis_port'],
234
- redis_password=database_config['redis_password'],
228
+ name=database_config.name,
229
+ redis_host=database_config.redis_host,
230
+ redis_port=database_config.redis_port,
231
+ redis_password=database_config.redis_password,
235
232
  ))
236
233
 
237
234
  return DatabaseManager(postgres_databases=postgres_databases, mongo_databases=mongo_databases, redis_databases=redis_databases)
@@ -0,0 +1,154 @@
1
+ """
2
+ 数据库迁移脚本 - 创建 feedback 表
3
+
4
+ 使用方法:
5
+ 1. 确保你的 service.yaml 中配置了数据库连接
6
+ 2. 运行此脚本: python -m src.service_forge.db.migrations.feedback_migration
7
+
8
+ 此脚本会:
9
+ - 检查 feedback 表是否存在
10
+ - 如果不存在,创建 feedback 表及索引
11
+ """
12
+
13
+ import asyncio
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ # 添加项目根目录到 Python 路径
18
+ project_root = Path(__file__).parent.parent.parent.parent
19
+ sys.path.insert(0, str(project_root))
20
+
21
+ from loguru import logger
22
+ from sqlalchemy import text
23
+ from src.service_forge.db.database import DatabaseManager
24
+ from src.service_forge.db.models.feedback import Base, FeedbackBase
25
+
26
+
27
+ async def create_database_if_not_exists(db):
28
+ """如果数据库不存在,则创建数据库"""
29
+ from sqlalchemy import create_engine
30
+ from sqlalchemy.exc import OperationalError
31
+ import asyncpg
32
+
33
+ try:
34
+ # 先尝试连接目标数据库
35
+ await db.init()
36
+ logger.info(f"✓ 数据库 '{db.postgres_db}' 已存在")
37
+ await db.close()
38
+ return True
39
+ except Exception as e:
40
+ logger.info(f"数据库 '{db.postgres_db}' 不存在,准备创建...")
41
+
42
+ try:
43
+ # 连接到 postgres 默认数据库
44
+ conn = await asyncpg.connect(
45
+ host=db.postgres_host,
46
+ port=db.postgres_port,
47
+ user=db.postgres_user,
48
+ password=db.postgres_password,
49
+ database='postgres'
50
+ )
51
+
52
+ # 创建数据库
53
+ await conn.execute(f'CREATE DATABASE {db.postgres_db}')
54
+ await conn.close()
55
+
56
+ logger.info(f"✓ 数据库 '{db.postgres_db}' 创建成功!")
57
+ return True
58
+
59
+ except Exception as create_error:
60
+ logger.error(f"创建数据库失败: {create_error}")
61
+ return False
62
+
63
+
64
+ async def create_feedback_table(database_manager: DatabaseManager):
65
+ """创建 feedback 表"""
66
+ db = database_manager.get_default_postgres_database()
67
+
68
+ if db is None:
69
+ logger.error("未找到默认 PostgreSQL 数据库配置")
70
+ return False
71
+
72
+ try:
73
+ # 1. 先确保数据库存在
74
+ db_created = await create_database_if_not_exists(db)
75
+ if not db_created:
76
+ return False
77
+
78
+ # 2. 连接到数据库
79
+ await db.init()
80
+ engine = db.engine
81
+
82
+ logger.info(f"连接到数据库: {db.database_url}")
83
+
84
+ # 3. 检查表是否存在
85
+ async with engine.begin() as conn:
86
+ result = await conn.execute(
87
+ text(
88
+ "SELECT EXISTS (SELECT FROM information_schema.tables "
89
+ "WHERE table_schema = 'public' AND table_name = 'feedback');"
90
+ )
91
+ )
92
+ table_exists = result.scalar()
93
+
94
+ if table_exists:
95
+ logger.info("✓ feedback 表已存在,跳过创建")
96
+ return True
97
+
98
+ logger.info("创建 feedback 表...")
99
+
100
+ # 4. 创建表
101
+ await conn.run_sync(Base.metadata.create_all)
102
+
103
+ logger.info("✓ feedback 表创建成功!")
104
+ logger.info("表结构:")
105
+ logger.info(" - feedback_id: UUID (主键)")
106
+ logger.info(" - task_id: VARCHAR(255) (索引)")
107
+ logger.info(" - workflow_name: VARCHAR(255) (索引)")
108
+ logger.info(" - rating: INTEGER (可选, 1-5)")
109
+ logger.info(" - comment: TEXT (可选)")
110
+ logger.info(" - metadata: JSONB (可选)")
111
+ logger.info(" - created_at: TIMESTAMP (索引)")
112
+ logger.info(" - updated_at: TIMESTAMP (可选)")
113
+
114
+ await db.close()
115
+ return True
116
+
117
+ except Exception as e:
118
+ logger.error(f"创建 feedback 表失败: {e}")
119
+ return False
120
+
121
+
122
+ async def main():
123
+ """主函数"""
124
+ logger.info("=== Feedback 表迁移脚本 ===")
125
+
126
+ # 提示用户提供配置文件路径
127
+ if len(sys.argv) > 1:
128
+ config_path = sys.argv[1]
129
+ else:
130
+ logger.info("请提供 service 配置文件路径:")
131
+ logger.info(" python -m src.service_forge.db.migrations.feedback_migration <config_path>")
132
+ logger.info("例如:")
133
+ logger.info(" python -m src.service_forge.db.migrations.feedback_migration configs/service/my_service.yaml")
134
+ return
135
+
136
+ logger.info(f"读取配置文件: {config_path}")
137
+
138
+ try:
139
+ database_manager = DatabaseManager.from_config(config_path=config_path)
140
+ success = await create_feedback_table(database_manager)
141
+
142
+ if success:
143
+ logger.info("✓ 迁移完成!")
144
+ else:
145
+ logger.error("✗ 迁移失败")
146
+ sys.exit(1)
147
+
148
+ except Exception as e:
149
+ logger.error(f"迁移过程出错: {e}")
150
+ sys.exit(1)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ asyncio.run(main())
File without changes
@@ -0,0 +1,33 @@
1
+ from datetime import datetime
2
+ from sqlalchemy import Column, String, Integer, DateTime, Text
3
+ from sqlalchemy.dialects.postgresql import UUID, JSONB
4
+ from sqlalchemy.ext.declarative import declarative_base
5
+ import uuid
6
+
7
+ Base = declarative_base()
8
+
9
+
10
+ class FeedbackBase(Base):
11
+ """反馈数据表模型"""
12
+ __tablename__ = "feedback"
13
+
14
+ feedback_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
15
+ task_id = Column(String(255), nullable=False, index=True)
16
+ workflow_name = Column(String(255), nullable=False, index=True)
17
+ rating = Column(Integer, nullable=True)
18
+ comment = Column(Text, nullable=True)
19
+ extra_metadata = Column("metadata", JSONB, nullable=True, default={})
20
+ created_at = Column(DateTime, nullable=False, default=datetime.now, index=True)
21
+ updated_at = Column(DateTime, nullable=True, onupdate=datetime.now)
22
+
23
+ def to_dict(self):
24
+ """转换为字典格式"""
25
+ return {
26
+ "feedback_id": str(self.feedback_id),
27
+ "task_id": self.task_id,
28
+ "workflow_name": self.workflow_name,
29
+ "rating": self.rating,
30
+ "comment": self.comment,
31
+ "metadata": self.extra_metadata or {},
32
+ "created_at": self.created_at,
33
+ }
@@ -15,6 +15,7 @@ class Model(Enum):
15
15
  DOUBO_SEED_1_6_FLASH_250615 = "doubao-seed-1-6-flash-250615"
16
16
  DEEPSEEK_V3_250324 = "deepseek-v3-250324"
17
17
  AZURE_GPT_4O_MINI = "azure-gpt-4o-mini"
18
+ GEMINI = "gemini-2.5-flash"
18
19
 
19
20
  def provider(self) -> str:
20
21
  if self.value.startswith("gpt"):
@@ -27,6 +28,8 @@ class Model(Enum):
27
28
  return "deepseek"
28
29
  elif self.value.startswith("azure"):
29
30
  return "azure"
31
+ elif self.value.startswith("gemini"):
32
+ return "gemini"
30
33
  raise ValueError(f"Invalid model: {self.value}")
31
34
 
32
35
  def get_model(model: str) -> Model:
@@ -51,6 +54,8 @@ def get_llm(provider: str) -> LLM:
51
54
  _llm_dicts[provider] = LLM(os.environ.get("DEEPSEEK_API_KEY", ""), os.environ.get("DEEPSEEK_BASE_URL", ""), int(os.environ.get("DEEPSEEK_TIMEOUT", 2000)))
52
55
  elif provider == "azure":
53
56
  _llm_dicts[provider] = LLM(os.environ.get("AZURE_API_KEY", ""), os.environ.get("AZURE_BASE_URL", ""), int(os.environ.get("AZURE_TIMEOUT", 2000)), os.environ.get("AZURE_API_VERSION", ""))
57
+ elif provider == "gemini":
58
+ _llm_dicts[provider] = LLM(os.environ.get("GEMINI_API_KEY", ""), os.environ.get("GEMINI_BASE_URL", ""), int(os.environ.get("GEMINI_TIMEOUT", 2000)))
54
59
  else:
55
60
  raise ValueError(f"Invalid provider: {provider}")
56
61
  return _llm_dicts[provider]
@@ -0,0 +1,30 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, Any
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+
7
+ class FeedbackCreate(BaseModel):
8
+ """创建反馈的请求模型"""
9
+ task_id: str = Field(..., description="工作流任务ID")
10
+ workflow_name: str = Field(..., description="工作流名称")
11
+ rating: Optional[int] = Field(None, ge=1, le=5, description="评分 (1-5)")
12
+ comment: Optional[str] = Field(None, description="用户评论")
13
+ metadata: Optional[dict[str, Any]] = Field(default_factory=dict, description="额外的元数据")
14
+
15
+
16
+ class FeedbackResponse(BaseModel):
17
+ """反馈响应模型"""
18
+ feedback_id: str = Field(..., description="反馈ID")
19
+ task_id: str = Field(..., description="工作流任务ID")
20
+ workflow_name: str = Field(..., description="工作流名称")
21
+ rating: Optional[int] = Field(None, description="评分")
22
+ comment: Optional[str] = Field(None, description="用户评论")
23
+ metadata: dict[str, Any] = Field(default_factory=dict, description="元数据")
24
+ created_at: datetime = Field(..., description="创建时间")
25
+
26
+
27
+ class FeedbackListResponse(BaseModel):
28
+ """反馈列表响应模型"""
29
+ total: int = Field(..., description="总数")
30
+ feedbacks: list[FeedbackResponse] = Field(..., description="反馈列表")