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.
- service_forge/api/http_api.py +4 -0
- service_forge/api/routers/feedback/feedback_router.py +148 -0
- service_forge/api/routers/service/service_router.py +22 -32
- service_forge/current_service.py +14 -0
- service_forge/db/database.py +29 -32
- service_forge/db/migrations/feedback_migration.py +154 -0
- service_forge/db/models/__init__.py +0 -0
- service_forge/db/models/feedback.py +33 -0
- service_forge/llm/__init__.py +5 -0
- service_forge/model/feedback.py +30 -0
- service_forge/service.py +118 -126
- service_forge/service_config.py +42 -156
- service_forge/sft/config/injector.py +33 -23
- service_forge/sft/config/sft_config.py +55 -8
- service_forge/storage/__init__.py +5 -0
- service_forge/storage/feedback_storage.py +245 -0
- service_forge/utils/workflow_clone.py +3 -2
- service_forge/workflow/node.py +8 -0
- service_forge/workflow/nodes/llm/query_llm_node.py +1 -1
- service_forge/workflow/trigger.py +4 -0
- service_forge/workflow/triggers/a2a_api_trigger.py +2 -0
- service_forge/workflow/triggers/fast_api_trigger.py +32 -0
- service_forge/workflow/triggers/kafka_api_trigger.py +3 -0
- service_forge/workflow/triggers/once_trigger.py +4 -1
- service_forge/workflow/triggers/period_trigger.py +4 -1
- service_forge/workflow/triggers/websocket_api_trigger.py +15 -11
- service_forge/workflow/workflow.py +26 -4
- service_forge/workflow/workflow_config.py +66 -0
- service_forge/workflow/workflow_factory.py +86 -85
- service_forge/workflow/workflow_group.py +33 -9
- {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/METADATA +1 -1
- {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/RECORD +34 -26
- service_forge/api/routers/service/__init__.py +0 -4
- {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/WHEEL +0 -0
- {service_forge-0.1.11.dist-info → service_forge-0.1.21.dist-info}/entry_points.txt +0 -0
service_forge/api/http_api.py
CHANGED
|
@@ -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/{
|
|
49
|
-
async def start_workflow(
|
|
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 =
|
|
45
|
+
success = service.start_workflow_by_id(uuid.UUID(workflow_id))
|
|
56
46
|
if success:
|
|
57
|
-
return WorkflowActionResponse(success=True, message=f"Workflow {
|
|
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 {
|
|
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 {
|
|
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/{
|
|
65
|
-
async def stop_workflow(
|
|
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.
|
|
61
|
+
success = await service.stop_workflow_by_id(uuid.UUID(workflow_id))
|
|
72
62
|
if success:
|
|
73
|
-
return WorkflowActionResponse(success=True, message=f"Workflow {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
workflow_id = await service.load_workflow_from_config(config=config)
|
|
116
105
|
|
|
117
|
-
if
|
|
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
|
service_forge/db/database.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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 (
|
|
202
|
-
('
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
208
|
+
if database_config.postgres_host is not None:
|
|
212
209
|
postgres_databases.append(PostgresDatabase(
|
|
213
|
-
name=database_config
|
|
214
|
-
postgres_user=database_config
|
|
215
|
-
postgres_password=database_config
|
|
216
|
-
postgres_host=database_config
|
|
217
|
-
postgres_port=database_config
|
|
218
|
-
postgres_db=database_config
|
|
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
|
-
|
|
217
|
+
elif database_config.mongo_host is not None:
|
|
221
218
|
mongo_databases.append(MongoDatabase(
|
|
222
|
-
name=database_config
|
|
223
|
-
mongo_host=database_config
|
|
224
|
-
mongo_port=database_config
|
|
225
|
-
mongo_user=database_config
|
|
226
|
-
mongo_password=database_config
|
|
227
|
-
mongo_db=database_config
|
|
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
|
-
|
|
226
|
+
elif database_config.redis_host is not None:
|
|
230
227
|
redis_databases.append(RedisDatabase(
|
|
231
|
-
name=database_config
|
|
232
|
-
redis_host=database_config
|
|
233
|
-
redis_port=database_config
|
|
234
|
-
redis_password=database_config
|
|
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
|
+
}
|
service_forge/llm/__init__.py
CHANGED
|
@@ -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="反馈列表")
|