[add] app chat v1

This commit is contained in:
Mark
2025-12-24 20:35:04 +08:00
parent 63d5047d21
commit bbd73d5e95
14 changed files with 1497 additions and 264 deletions

View File

@@ -361,7 +361,8 @@ async def draft_run(
workspace_id=workspace_id, workspace_id=workspace_id,
user=current_user user=current_user
) )
if storage_type is None: storage_type = 'neo4j' if storage_type is None:
storage_type = 'neo4j'
user_rag_memory_id = '' user_rag_memory_id = ''
if workspace_id: if workspace_id:
@@ -370,7 +371,8 @@ async def draft_run(
name="USER_RAG_MERORY", name="USER_RAG_MERORY",
workspace_id=workspace_id workspace_id=workspace_id
) )
if knowledge: user_rag_memory_id = str(knowledge.id) if knowledge:
user_rag_memory_id = str(knowledge.id)
# 提前验证和准备(在流式响应开始前完成) # 提前验证和准备(在流式响应开始前完成)

View File

@@ -0,0 +1,97 @@
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
import os
from app.db import get_db
from app.dependencies import get_current_user
from app.models.user_model import User
from app.schemas.order_schema import CreateOrderRequest
from app.schemas.response_schema import ApiResponse
from app.services.order_service import get_order_service
from app.core.logging_config import get_api_logger
from app.core.response_utils import success, error
# Get API logger
api_logger = get_api_logger()
router = APIRouter(
prefix="/order",
tags=["Order"],
)
@router.post("", response_model=ApiResponse)
async def create_order(
order_data: CreateOrderRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
try:
api_logger.info(f"User {current_user.id} creating order for product {order_data.product_id}")
# Get external API configuration from environment
external_api_url = os.getenv("EXTERNAL_ORDER_API_URL")
api_key = os.getenv("EXTERNAL_ORDER_API_KEY")
# Get order service instance
order_service = get_order_service(
external_api_url=external_api_url,
api_key=api_key
)
# Forward request to external API
result = await order_service.create_order(
order_data=order_data,
user_id=str(current_user.id)
)
api_logger.info(f"Order created successfully: {result.get('order_id')}")
return success(data=result, msg="Order created successfully")
except Exception as e:
api_logger.error(f"Failed to create order: {str(e)}", exc_info=True)
return error(msg=str(e), code=status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.get("/{order_id}", response_model=ApiResponse)
async def get_order(
order_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get order details from external API
Args:
order_id: Order ID
db: Database session
current_user: Current authenticated user
Returns:
API response with order details
"""
try:
api_logger.info(f"User {current_user.id} fetching order {order_id}")
# Get external API configuration
external_api_url = os.getenv("EXTERNAL_ORDER_API_URL")
api_key = os.getenv("EXTERNAL_ORDER_API_KEY")
# Get order service instance
order_service = get_order_service(
external_api_url=external_api_url,
api_key=api_key
)
# Fetch order from external API
result = await order_service.get_order(order_id)
api_logger.info(f"Order {order_id} fetched successfully")
return success(data=result, msg="Order fetched successfully")
except Exception as e:
api_logger.error(f"Failed to fetch order {order_id}: {str(e)}", exc_info=True)
return error(msg=str(e), code=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -2,14 +2,30 @@
import uuid import uuid
from fastapi import APIRouter, Depends, Request, Body from fastapi import APIRouter, Depends, Request, Body
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Optional, Annotated
from starlette.responses import StreamingResponse
from app.core.api_key_auth import require_api_key
from app.db import get_db from app.db import get_db
from app.core.response_utils import success from app.core.response_utils import success
from app.core.logging_config import get_business_logger from app.core.logging_config import get_business_logger
from app.core.api_key_auth import require_api_key from app.dependencies import get_app_or_workspace
from app.schemas.api_key_schema import ApiKeyAuth from app.repositories import knowledge_repository
from app.schemas import AppChatRequest, conversation_schema
from app.models.app_model import App
from app.models.app_model import AppType
from app.repositories.end_user_repository import EndUserRepository
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.services import workspace_service
from app.services.app_chat_service import AppChatService, get_app_chat_service
from app.services.app_service import AppService
from app.services.conversation_service import ConversationService, get_conversation_service
from app.services.workflow_service import WorkflowService, get_workflow_service
from app.utils.app_config_utils import dict_to_multi_agent_config,dict_to_agent_config,dict_to_workflow_config
router = APIRouter(prefix="/apps", tags=["V1 - App API"]) router = APIRouter(prefix="/app", tags=["V1 - App API"])
logger = get_business_logger() logger = get_business_logger()
@@ -19,28 +35,232 @@ async def list_apps():
return success(data=[], msg="App API - Coming Soon") return success(data=[], msg="App API - Coming Soon")
# /v1/apps/{resource_id}/chat # /v1/apps/{resource_id}/chat
@router.post("/{resource_id}/chat")
@require_api_key(scopes=["app"])
async def chat_with_agent_demo( # async def chat(
resource_id: uuid.UUID, # request: Request,
request: Request, # api_key_auth: ApiKeyAuth = None,
api_key_auth: ApiKeyAuth = None, # db: Session = Depends(get_db),
# message: str = Body(..., description="聊天消息内容"),
# ):
# """
# Agent 聊天接口demo
# scopes: 所需的权限范围列表["app", "rag", "memory"]
# Args:
# resource_id: 如果是应用的apikey传的是应用id; 如果是服务的apikey传的是工作空间id
# message: 请求参数
# request: 声明请求
# api_key_auth: 包含验证后的API Key 信息
# db: db_session
# """
# logger.info(f"API Key Auth: {api_key_auth}")
# logger.info(f"Resource ID: {resource_id}")
# logger.info(f"Message: {message}")
# return success(data={"received": True}, msg="消息已接收")
def _checkAppConfig(app: App):
if app.type == AppType.AGENT:
if not app.current_release.config:
raise BusinessException("Agent 应用未配置模型", BizCode.AGENT_CONFIG_MISSING)
elif app.type == AppType.MULTI_AGENT:
if not app.current_release.config:
raise BusinessException("Multi-Agent 应用未配置模型", BizCode.AGENT_CONFIG_MISSING)
elif app.type == AppType.WORKFLOW:
if not app.current_release.config:
raise BusinessException("工作流应用未配置模型", BizCode.AGENT_CONFIG_MISSING)
else:
raise BusinessException("不支持的应用类型", BizCode.AGENT_CONFIG_MISSING)
@router.post("/chat")
# @require_api_key(scopes=["app"])
async def chat(
payload: AppChatRequest,
app: App = Depends(get_app_or_workspace),
db: Session = Depends(get_db), db: Session = Depends(get_db),
message: str = Body(..., description="聊天消息内容"), conversation_service: Annotated[ConversationService, Depends(get_conversation_service)] = None,
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
): ):
""" other_id = payload.user_id
Agent 聊天接口demo workspace_id = app.workspace_id
end_user_repo = EndUserRepository(db)
new_end_user = end_user_repo.get_or_create_end_user(
app_id=app.id,
other_id=other_id,
original_user_id=other_id # Save original user_id to other_id
)
end_user_id = str(new_end_user.id)
scopes: 所需的权限范围列表["app", "rag", "memory"] # 提前验证和准备(在流式响应开始前完成)
storage_type = workspace_service.get_workspace_storage_type_without_auth(
db=db,
workspace_id=workspace_id
)
if storage_type is None:
storage_type = 'neo4j'
user_rag_memory_id = ''
if storage_type == 'rag':
if workspace_id:
knowledge = knowledge_repository.get_knowledge_by_name(
db=db,
name="USER_RAG_MERORY",
workspace_id=workspace_id
)
if knowledge:
user_rag_memory_id = str(knowledge.id)
else:
logger.warning(
f"未找到名为 'USER_RAG_MERORY' 的知识库workspace_id: {workspace_id},将使用 neo4j 存储")
storage_type = 'neo4j'
else:
logger.warning("workspace_id 为空,无法使用 rag 存储,将使用 neo4j 存储")
storage_type = 'neo4j'
app_type = app.type
# check app config
_checkAppConfig(app)
Args: # 获取或创建会话(提前验证)
resource_id: 如果是应用的apikey传的是应用id; 如果是服务的apikey传的是工作空间id conversation = conversation_service.create_or_get_conversation(
message: 请求参数 app_id=app.id,
request: 声明请求 workspace_id=workspace_id,
api_key_auth: 包含验证后的API Key 信息 user_id=end_user_id,
db: db_session is_draft=False
""" )
logger.info(f"API Key Auth: {api_key_auth}")
logger.info(f"Resource ID: {resource_id}") if app_type == AppType.AGENT:
logger.info(f"Message: {message}") agent_config = dict_to_agent_config(app.current_release.config)
return success(data={"received": True}, msg="消息已接收") # 流式返回
if payload.stream:
async def event_generator():
async for event in app_chat_service.agnet_chat_stream(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id= end_user_id, # 转换为字符串
variables=payload.variables,
web_search=payload.web_search,
config=app.current_release.config,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
):
yield event
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
# 非流式返回
result = await app_chat_service.agnet_chat(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config= agent_config,
web_search=payload.web_search,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
elif app_type == AppType.MULTI_AGENT:
# 多 Agent 流式返回
config = dict_to_multi_agent_config(app.current_release.config)
if payload.stream:
async def event_generator():
async for event in app_chat_service.multi_agent_chat_stream(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
):
yield event
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
# 多 Agent 非流式返回
result = await app_chat_service.multi_agent_chat(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
elif app_type == AppType.WORKFLOW:
# 多 Agent 流式返回
config = dict_to_workflow_config(app.current_release.config)
if payload.stream:
async def event_generator():
async for event in app_chat_service.workflow_chat_stream(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
):
yield event
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
# 非流式返回
result = await app_chat_service.workflow_chat(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
else:
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
raise BusinessException(f"不支持的应用类型: {app_type}", BizCode.APP_TYPE_NOT_SUPPORTED)
pass

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import uuid import uuid

View File

@@ -1,12 +1,13 @@
import uuid import uuid
from functools import wraps from functools import wraps
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from jose import jwt, JWTError from jose import jwt, JWTError
from app.db import get_db, SessionLocal from app.db import get_db, SessionLocal
from app.models import App
from app.schemas import token_schema from app.schemas import token_schema
from app.core.config import settings from app.core.config import settings
from app.core.security import get_token_id from app.core.security import get_token_id
@@ -27,6 +28,51 @@ security_logger = get_security_logger()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class APIKeyExtractor:
"""
Custom dependency to extract API Key from request headers
Supports two formats:
1. Authorization: Bearer <api_key>
2. X-API-Key: <api_key>
"""
async def __call__(self, request: Request) -> str:
"""Extract API Key from request headers
Args:
request: FastAPI Request object
Returns:
API Key string
Raises:
HTTPException: If API Key is not found
"""
# Try Authorization header first
auth_header = request.headers.get("Authorization")
if auth_header and " " in auth_header:
auth_scheme, auth_token = auth_header.split(" ", 1)
if auth_scheme.lower() == "bearer":
return auth_token
# Try X-API-Key header
api_key = request.headers.get("X-API-Key")
if api_key:
return api_key
# No API Key found
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API Key not found in request headers",
headers={"WWW-Authenticate": "Bearer"},
)
api_key_extractor = APIKeyExtractor()
async def get_current_user( async def get_current_user(
token: str = Depends(oauth2_scheme), token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -469,4 +515,75 @@ async def get_share_user_id(
raise credentials_exception raise credentials_exception
async def get_app_or_workspace(
api_key: str = Depends(api_key_extractor),
db: Session = Depends(get_db)
) -> App | Workspace:
"""
Get App or Workspace from API Key
Supports two API Key formats:
1. Authorization: Bearer <api_key>
2. X-API-Key: <api_key>
Args:
api_key: API Key extracted from request headers
db: Database session
Returns:
App or Workspace object based on API Key
Raises:
HTTPException: If API Key is invalid or not found
"""
from app.services.api_key_service import ApiKeyAuthService
from app.repositories.app_repository import get_apps_by_id
from app.repositories.workspace_repository import get_workspace_by_id
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate API Key",
headers={"WWW-Authenticate": "Bearer"},
)
try:
auth_logger.debug(f"Validating API Key: {api_key[:10]}...")
# Validate API Key
api_key_obj = ApiKeyAuthService.validate_api_key(db, api_key)
if not api_key_obj:
auth_logger.warning(f"Invalid or expired API Key: {api_key[:10]}...")
raise credentials_exception
auth_logger.debug(f"API Key validated successfully, type: {api_key_obj.type}")
# Return App or Workspace based on API Key type
if (api_key_obj.type == "agent" or api_key.type == "multi_agent") and api_key_obj.resource_id:
# App API Key
app = get_apps_by_id(db, api_key_obj.resource_id)
if not app:
auth_logger.warning(f"App not found for API Key: {api_key_obj.resource_id}")
raise credentials_exception
auth_logger.info(f"App access granted: {app.id}")
return app
elif api_key_obj.type == "service":
# Workspace API Key
workspace = get_workspace_by_id(db, api_key_obj.workspace_id)
if not workspace:
auth_logger.warning(f"Workspace not found for API Key: {api_key_obj.workspace_id}")
raise credentials_exception
auth_logger.info(f"Workspace access granted: {workspace.id}")
return workspace
else:
auth_logger.warning(f"Unsupported API Key type: {api_key_obj.type}")
raise credentials_exception
except HTTPException:
raise
except Exception as e:
auth_logger.error(f"Error validating API Key: {str(e)}", exc_info=True)
raise credentials_exception

View File

@@ -2,38 +2,10 @@ import os
import subprocess import subprocess
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, APIRouter
from fastapi import HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.core.response_utils import fail
from app.core.logging_config import LoggingConfig, get_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode, HTTP_MAPPING
from app.controllers import (
model_controller,
task_controller,
test_controller,
user_controller,
auth_controller,
workspace_controller,
setup_controller,
file_controller,
document_controller,
knowledge_controller,
chunk_controller,
knowledgeshare_controller,
app_controller,
upload_controller,
memory_agent_controller,
memory_storage_controller,
memory_dashboard_controller,
multi_agent_controller,
)
from fastapi import FastAPI, APIRouter
app = FastAPI(title="Data Config API", version="1.0.0")
router = APIRouter(prefix="/memory", tags=["Memory"])
# 管理端 API (JWT 认证) # 管理端 API (JWT 认证)
from app.controllers import manager_router from app.controllers import manager_router

View File

@@ -8,7 +8,9 @@ from .file_schema import File, FileCreate, FileUpdate
from .tenant_schema import Tenant, TenantCreate, TenantUpdate from .tenant_schema import Tenant, TenantCreate, TenantUpdate
from .chunk_schema import ChunkCreate, ChunkUpdate, ChunkRetrieve from .chunk_schema import ChunkCreate, ChunkUpdate, ChunkRetrieve
from .knowledgeshare_schema import KnowledgeShare, KnowledgeShareCreate from .knowledgeshare_schema import KnowledgeShare, KnowledgeShareCreate
from .order_schema import CreateOrderRequest, OrderResponse, ExternalOrderResponse
from .app_schema import ( from .app_schema import (
AppChatRequest,
DraftRunRequest, DraftRunRequest,
DraftRunResponse, DraftRunResponse,
DraftRunStreamChunk, DraftRunStreamChunk,
@@ -73,6 +75,10 @@ __all__ = [
"ChunkRetrieve", "ChunkRetrieve",
"KnowledgeShare", "KnowledgeShare",
"KnowledgeShareCreate", "KnowledgeShareCreate",
"CreateOrderRequest",
"OrderResponse",
"ExternalOrderResponse",
"AppChatRequest",
"DraftRunRequest", "DraftRunRequest",
"DraftRunResponse", "DraftRunResponse",
"DraftRunStreamChunk", "DraftRunStreamChunk",

View File

@@ -334,6 +334,13 @@ class AppShare(BaseModel):
# ---------- Draft Run Schemas ---------- # ---------- Draft Run Schemas ----------
class AppChatRequest(BaseModel):
message: str = Field(..., description="用户消息")
conversation_id: Optional[str] = Field(default=None, description="会话ID用于多轮对话")
user_id: Optional[str] = Field(default=None, description="用户ID用于会话管理")
variables: Optional[Dict[str, Any]] = Field(default=None, description="自定义变量参数值")
stream: bool = Field(default=False, description="是否流式返回")
class DraftRunRequest(BaseModel): class DraftRunRequest(BaseModel):
"""试运行请求""" """试运行请求"""
message: str = Field(..., description="用户消息") message: str = Field(..., description="用户消息")

View File

@@ -0,0 +1,63 @@
"""
Order Schema
Defines request and response models for order operations.
"""
from pydantic import BaseModel, Field
from typing import Any, Optional
class CreateOrderRequest(BaseModel):
"""Create order request model"""
product_id: str = Field(..., description="Product ID")
quantity: int = Field(..., gt=0, description="Order quantity")
customer_name: Optional[str] = Field(None, description="Customer name")
customer_email: Optional[str] = Field(None, description="Customer email")
notes: Optional[str] = Field(None, description="Order notes")
class Config:
json_schema_extra = {
"example": {
"product_id": "PROD-001",
"quantity": 2,
"customer_name": "John Doe",
"customer_email": "john@example.com",
"notes": "Please deliver before 5pm"
}
}
class OrderResponse(BaseModel):
"""Order response model"""
order_id: str = Field(..., description="Order ID")
status: str = Field(..., description="Order status")
product_id: str = Field(..., description="Product ID")
quantity: int = Field(..., description="Order quantity")
total_amount: Optional[float] = Field(None, description="Total amount")
created_at: Optional[str] = Field(None, description="Creation timestamp")
message: Optional[str] = Field(None, description="Response message")
class Config:
json_schema_extra = {
"example": {
"order_id": "ORD-20231224-001",
"status": "pending",
"product_id": "PROD-001",
"quantity": 2,
"total_amount": 199.99,
"created_at": "2023-12-24T10:30:00Z",
"message": "Order created successfully"
}
}
class ExternalOrderResponse(BaseModel):
"""External API response model (flexible structure)"""
success: bool = Field(default=True, description="Request success status")
data: Optional[Any] = Field(None, description="Response data")
error: Optional[str] = Field(None, description="Error message")
code: Optional[int] = Field(None, description="Response code")

View File

@@ -0,0 +1,485 @@
"""基于分享链接的聊天服务"""
import asyncio
import json
import time
import uuid
from typing import Optional, Dict, Any, AsyncGenerator, Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from app.core.agent.langchain_agent import LangChainAgent
from app.core.logging_config import get_business_logger
from app.db import get_db
from app.models import MultiAgentConfig, AgentConfig
from app.schemas.prompt_schema import render_prompt_message, PromptMessageRole
from app.services.conversation_service import ConversationService
from app.services.draft_run_service import create_knowledge_retrieval_tool, create_long_term_memory_tool
from app.services.draft_run_service import create_web_search_tool
from app.services.model_service import ModelApiKeyService
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
logger = get_business_logger()
class AppChatService:
"""基于分享链接的聊天服务"""
def __init__(self, db: Session):
self.db = db
self.conversation_service = ConversationService(db)
async def agnet_chat(
self,
message: str,
conversation_id: uuid.UUID,
config: AgentConfig,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
web_search: bool = False,
memory: bool = True,
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
) -> Dict[str, Any]:
"""聊天(非流式)"""
start_time = time.time()
config_id = None
if variables is None:
variables = {}
# 获取模型配置ID
model_config_id = config.default_model_config_id
api_key_obj = ModelApiKeyService.get_a_api_key(model_config_id)
# 处理系统提示词(支持变量替换)
system_prompt = config.get("system_prompt", "")
if variables:
system_prompt_rendered = render_prompt_message(
system_prompt,
PromptMessageRole.USER,
variables
)
system_prompt = system_prompt_rendered.get_text_content() or system_prompt
# 准备工具列表
tools = []
# 添加知识库检索工具
knowledge_retrieval = config.get("knowledge_retrieval")
if knowledge_retrieval:
knowledge_bases = knowledge_retrieval.get("knowledge_bases", [])
kb_ids = [kb.get("kb_id") for kb in knowledge_bases if kb.get("kb_id")]
if kb_ids:
kb_tool = create_knowledge_retrieval_tool(knowledge_retrieval, kb_ids, user_id)
tools.append(kb_tool)
# 添加长期记忆工具
memory_flag = False
if memory == True:
memory_config = config.get("memory", {})
if memory_config.get("enabled") and user_id:
memory_flag = True
memory_tool = create_long_term_memory_tool(memory_config, user_id)
tools.append(memory_tool)
web_tools = config.get("tools")
web_search_choice = web_tools.get("web_search", {})
web_search_enable = web_search_choice.get("enabled", False)
if web_search == True:
if web_search_enable == True:
search_tool = create_web_search_tool({})
tools.append(search_tool)
logger.debug(
"已添加网络搜索工具",
extra={
"tool_count": len(tools)
}
)
# 获取模型参数
model_parameters = config.get("model_parameters", {})
# 创建 LangChain Agent
agent = LangChainAgent(
model_name=api_key_obj.model_name,
api_key=api_key_obj.api_key,
provider=api_key_obj.provider,
api_base=api_key_obj.api_base,
temperature=model_parameters.get("temperature", 0.7),
max_tokens=model_parameters.get("max_tokens", 2000),
system_prompt=system_prompt,
tools=tools,
)
# 加载历史消息
history = []
memory_config = {"enabled": True, 'max_history': 10}
if memory_config.get("enabled"):
messages = self.conversation_service.get_messages(
conversation_id=conversation_id,
limit=memory_config.get("max_history", 10)
)
history = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
# 调用 Agent
result = await agent.chat(
message=message,
history=history,
context=None,
end_user_id=user_id,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
config_id=config_id,
memory_flag=memory_flag
)
# 保存消息
self.conversation_service.save_conversation_messages(
conversation_id=conversation_id,
user_message=message,
assistant_message=result["content"]
)
elapsed_time = time.time() - start_time
return {
"conversation_id": conversation_id,
"message": result["content"],
"usage": result.get("usage", {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}),
"elapsed_time": elapsed_time
}
async def agnet_chat_stream(
self,
message: str,
conversation_id: uuid.UUID,
config: AgentConfig,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
web_search: bool = False,
memory: bool = True,
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
) -> AsyncGenerator[str, None]:
"""聊天(流式)"""
try:
start_time = time.time()
config_id = None
if variables is None:
variables = {}
# 获取模型配置ID
model_config_id = config.default_model_config_id
api_key_obj = ModelApiKeyService.get_a_api_key(model_config_id)
# 处理系统提示词(支持变量替换)
system_prompt = config.get("system_prompt", "")
if variables:
system_prompt_rendered = render_prompt_message(
system_prompt,
PromptMessageRole.USER,
variables
)
system_prompt = system_prompt_rendered.get_text_content() or system_prompt
# 准备工具列表
tools = []
# 添加知识库检索工具
knowledge_retrieval = config.get("knowledge_retrieval")
if knowledge_retrieval:
knowledge_bases = knowledge_retrieval.get("knowledge_bases", [])
kb_ids = [kb.get("kb_id") for kb in knowledge_bases if kb.get("kb_id")]
if kb_ids:
kb_tool = create_knowledge_retrieval_tool(knowledge_retrieval, kb_ids, user_id)
tools.append(kb_tool)
# 添加长期记忆工具
memory_flag = False
if memory:
memory_config = config.get("memory", {})
if memory_config.get("enabled") and user_id:
memory_flag = True
memory_tool = create_long_term_memory_tool(memory_config, user_id)
tools.append(memory_tool)
web_tools = config.get("tools")
web_search_choice = web_tools.get("web_search", {})
web_search_enable = web_search_choice.get("enabled", False)
if web_search == True:
if web_search_enable == True:
search_tool = create_web_search_tool({})
tools.append(search_tool)
logger.debug(
"已添加网络搜索工具",
extra={
"tool_count": len(tools)
}
)
# 获取模型参数
model_parameters = config.get("model_parameters", {})
# 创建 LangChain Agent
agent = LangChainAgent(
model_name=api_key_obj.model_name,
api_key=api_key_obj.api_key,
provider=api_key_obj.provider,
api_base=api_key_obj.api_base,
temperature=model_parameters.get("temperature", 0.7),
max_tokens=model_parameters.get("max_tokens", 2000),
system_prompt=system_prompt,
tools=tools,
streaming=True
)
# 加载历史消息
history = []
memory_config = {"enabled": True, 'max_history': 10}
if memory_config.get("enabled"):
messages = self.conversation_service.get_messages(
conversation_id=conversation_id,
limit=memory_config.get("max_history", 10)
)
history = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
# 发送开始事件
yield f"event: start\ndata: {json.dumps({'conversation_id': str(conversation_id)}, ensure_ascii=False)}\n\n"
# 流式调用 Agent
full_content = ""
async for chunk in agent.chat_stream(
message=message,
history=history,
context=None,
end_user_id=user_id,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
config_id=config_id,
memory_flag=memory_flag
):
full_content += chunk
# 发送消息块事件
yield f"event: message\ndata: {json.dumps({'content': chunk}, ensure_ascii=False)}\n\n"
elapsed_time = time.time() - start_time
# 保存消息
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message
)
self.conversation_service.add_message(
conversation_id=conversation_id,
role="assistant",
content=full_content,
meta_data={
"model": api_key_obj.model_name,
"usage": {}
}
)
# 发送结束事件
end_data = {"elapsed_time": elapsed_time, "message_length": len(full_content)}
yield f"event: end\ndata: {json.dumps(end_data, ensure_ascii=False)}\n\n"
logger.info(
"流式聊天完成",
extra={
"conversation_id": str(conversation_id),
"elapsed_time": elapsed_time,
"message_length": len(full_content)
}
)
except (GeneratorExit, asyncio.CancelledError):
# 生成器被关闭或任务被取消,正常退出
logger.debug("流式聊天被中断")
raise
except Exception as e:
logger.error(f"流式聊天失败: {str(e)}", exc_info=True)
# 发送错误事件
yield f"event: error\ndata: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
async def multi_agent_chat(
self,
message: str,
conversation_id: uuid.UUID,
config: MultiAgentConfig,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
web_search: bool = False,
memory: bool = True,
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
) -> Dict[str, Any]:
"""多 Agent 聊天(非流式)"""
start_time = time.time()
actual_config_id = None
config_id = actual_config_id
if variables is None:
variables = {}
# 2. 创建编排器
orchestrator = MultiAgentOrchestrator(self.db, config)
# 3. 执行任务
result = await orchestrator.execute(
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
use_llm_routing=True, # 默认启用 LLM 路由
web_search=web_search, # 网络搜索参数
memory=memory # 记忆功能参数
)
elapsed_time = time.time() - start_time
# 保存消息
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message
)
self.conversation_service.add_message(
conversation_id=conversation_id,
role="assistant",
content=result.get("message", ""),
meta_data={
"mode": result.get("mode"),
"elapsed_time": result.get("elapsed_time"),
"sub_results": result.get("sub_results")
}
)
return {
"conversation_id": conversation_id,
"message": result.get("message", ""),
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
},
"elapsed_time": elapsed_time
}
async def multi_agent_chat_stream(
self,
message: str,
conversation_id: uuid.UUID,
config: MultiAgentConfig,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
web_search: bool = False,
memory: bool = True,
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
) -> AsyncGenerator[str, None]:
"""多 Agent 聊天(流式)"""
start_time = time.time()
actual_config_id = None
config_id = actual_config_id
if variables is None:
variables = {}
try:
# 发送开始事件
yield f"event: start\ndata: {json.dumps({'conversation_id': str(conversation_id)}, ensure_ascii=False)}\n\n"
full_content = ""
# 2. 创建编排器
orchestrator = MultiAgentOrchestrator(self.db, config)
# 3. 流式执行任务
async for event in orchestrator.execute_stream(
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
use_llm_routing=True,
web_search=web_search, # 网络搜索参数
memory=memory, # 记忆功能参数
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
):
yield event
# 尝试提取内容(用于保存)
if "data:" in event:
try:
data_line = event.split("data: ", 1)[1].strip()
data = json.loads(data_line)
if "content" in data:
full_content += data["content"]
except:
pass
elapsed_time = time.time() - start_time
# 保存消息
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message
)
self.conversation_service.add_message(
conversation_id=conversation_id,
role="assistant",
content=full_content,
meta_data={
"elapsed_time": elapsed_time
}
)
logger.info(
"多 Agent 流式聊天完成",
extra={
"conversation_id": str(conversation_id),
"elapsed_time": elapsed_time,
"message_length": len(full_content)
}
)
except (GeneratorExit, asyncio.CancelledError):
# 生成器被关闭或任务被取消,正常退出
logger.debug("多 Agent 流式聊天被中断")
raise
except Exception as e:
logger.error(f"多 Agent 流式聊天失败: {str(e)}", exc_info=True)
# 发送错误事件
yield f"event: error\ndata: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
# ==================== 依赖注入函数 ====================
def get_app_chat_service(
db: Annotated[Session, Depends(get_db)]
) -> ChatService:
"""获取工作流服务(依赖注入)"""
return ChatService(db)

View File

@@ -1,9 +1,12 @@
"""会话服务""" """会话服务"""
import uuid import uuid
from typing import Optional, List, Tuple from typing import Optional, List, Tuple, Annotated
from fastapi import Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select, desc from sqlalchemy import select, desc
from app.db import get_db
from app.models import Conversation, Message from app.models import Conversation, Message
from app.core.exceptions import ResourceNotFoundException, BusinessException from app.core.exceptions import ResourceNotFoundException, BusinessException
from app.core.error_codes import BizCode from app.core.error_codes import BizCode
@@ -227,3 +230,53 @@ class ConversationService:
"workspace_id": str(workspace_id) "workspace_id": str(workspace_id)
} }
) )
def create_or_get_conversation(
self,
app_id: uuid.UUID,
workspace_id: uuid.UUID,
is_draft: bool = False,
conversation_id: Optional[uuid.UUID] = None,
user_id: Optional[str] = None,
) -> Conversation:
"""创建或获取会话"""
# 如果提供了 conversation_id尝试获取现有会话
if conversation_id:
try:
conversation = self.get_conversation(
conversation_id=conversation_id,
workspace_id=workspace_id
)
# 验证会话是否属于该应用
if conversation.app_id != app_id:
raise BusinessException("会话不属于该应用", BizCode.INVALID_CONVERSATION)
return conversation
except ResourceNotFoundException:
logger.warning(
"会话不存在,将创建新会话",
extra={"conversation_id": str(conversation_id)}
)
# 创建新会话(使用发布版本的配置)
conversation = self.create_conversation(
app_id=app_id,
workspace_id=workspace_id,
user_id=user_id,
is_draft=is_draft
)
logger.info(
"为分享链接创建新会话"
)
return conversation
# ==================== 依赖注入函数 ====================
def get_conversation_service(
db: Annotated[Session, Depends(get_db)]
) -> ConversationService:
"""获取工作流服务(依赖注入)"""
return ConversationService(db)

View File

@@ -316,7 +316,7 @@ class ModelApiKeyService:
return api_key return api_key
@staticmethod @staticmethod
def get_api_keys_by_model(db: Session, model_config_id: uuid.UUID, is_active: bool = True) -> List[ModelApiKey]: def get_api_keys_by_model(db: Session, model_config_id: uuid.UUID, is_active: bool = True) -> list[ModelApiKey]:
"""根据模型配置ID获取API Key列表""" """根据模型配置ID获取API Key列表"""
if not ModelConfigRepository.get_by_id(db, model_config_id): if not ModelConfigRepository.get_by_id(db, model_config_id):
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND) raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
@@ -409,3 +409,11 @@ class ModelApiKeyService:
if success: if success:
db.commit() db.commit()
return success return success
@staticmethod
def get_a_api_key(db: Session, model_config_id: uuid.UUID) -> ModelApiKey:
api_kes = ModelApiKeyService.get_api_keys_by_model(db, model_config_id)
if api_kes and len(api_kes) > 0:
return api_kes[0]
raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING)

View File

@@ -0,0 +1,205 @@
"""
Order Service
Handles order operations including forwarding requests to external APIs.
"""
import logging
import httpx
from typing import Dict, Any, Optional
from app.schemas.order_schema import CreateOrderRequest
logger = logging.getLogger(__name__)
class OrderService:
"""Order service for handling order operations"""
def __init__(self, external_api_url: Optional[str] = None, api_key: Optional[str] = None):
"""Initialize order service
Args:
external_api_url: External API base URL
api_key: API key for authentication
"""
# Default external API URL (replace with actual URL)
self.external_api_url = external_api_url or "https://api.example.com/v1"
self.api_key = api_key
self.timeout = 30.0 # 30 seconds timeout
async def create_order(
self,
order_data: CreateOrderRequest,
user_id: str
) -> Dict[str, Any]:
"""Create order by forwarding request to external API
Args:
order_data: Order creation data
user_id: Current user ID
Returns:
Order response data
Raises:
httpx.HTTPError: If external API request fails
Exception: For other errors
"""
try:
# Prepare request payload
payload = {
"product_id": order_data.product_id,
"quantity": order_data.quantity,
"customer_name": order_data.customer_name,
"customer_email": order_data.customer_email,
"notes": order_data.notes,
"user_id": user_id # Include user ID for tracking
}
# Prepare headers
headers = {
"Content-Type": "application/json",
"User-Agent": "MemoryBear-OrderService/1.0"
}
# Add API key if configured
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
logger.info(f"Forwarding order creation request to external API: {self.external_api_url}/orders")
logger.debug(f"Request payload: {payload}")
# Make async HTTP request to external API
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.external_api_url}/orders",
json=payload,
headers=headers
)
# Log response status
logger.info(f"External API response status: {response.status_code}")
# Raise exception for 4xx/5xx status codes
response.raise_for_status()
# Parse response
response_data = response.json()
logger.debug(f"External API response data: {response_data}")
# Transform external API response to internal format
return self._transform_external_response(response_data)
except httpx.HTTPStatusError as e:
logger.error(f"External API returned error status: {e.response.status_code}")
logger.error(f"Error response: {e.response.text}")
# Try to parse error response
try:
error_data = e.response.json()
error_message = error_data.get("message") or error_data.get("error") or "External API error"
except Exception:
error_message = f"External API error: {e.response.status_code}"
raise Exception(f"Failed to create order: {error_message}")
except httpx.TimeoutException:
logger.error(f"External API request timeout after {self.timeout}s")
raise Exception("Order creation timeout - external service not responding")
except httpx.RequestError as e:
logger.error(f"External API request failed: {str(e)}")
raise Exception(f"Failed to connect to external order service: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error during order creation: {str(e)}", exc_info=True)
raise Exception(f"Order creation failed: {str(e)}")
def _transform_external_response(self, external_data: Dict[str, Any]) -> Dict[str, Any]:
"""Transform external API response to internal format
Args:
external_data: Response data from external API
Returns:
Transformed response data
"""
# Handle different response formats from external API
# Adjust this based on actual external API response structure
if "data" in external_data:
# Format 1: {"success": true, "data": {...}}
data = external_data["data"]
elif "order" in external_data:
# Format 2: {"order": {...}}
data = external_data["order"]
else:
# Format 3: Direct response
data = external_data
# Extract fields with fallbacks
return {
"order_id": data.get("order_id") or data.get("id") or "UNKNOWN",
"status": data.get("status") or "pending",
"product_id": data.get("product_id") or "",
"quantity": data.get("quantity") or 0,
"total_amount": data.get("total_amount") or data.get("amount"),
"created_at": data.get("created_at") or data.get("timestamp"),
"message": external_data.get("message") or "Order created successfully"
}
async def get_order(self, order_id: str) -> Dict[str, Any]:
"""Get order details from external API
Args:
order_id: Order ID
Returns:
Order details
"""
try:
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
logger.info(f"Fetching order {order_id} from external API")
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
f"{self.external_api_url}/orders/{order_id}",
headers=headers
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to fetch order {order_id}: {str(e)}")
raise Exception(f"Failed to fetch order: {str(e)}")
# Singleton instance
_order_service_instance: Optional[OrderService] = None
def get_order_service(
external_api_url: Optional[str] = None,
api_key: Optional[str] = None
) -> OrderService:
"""Get order service instance
Args:
external_api_url: External API URL (optional, uses default if not provided)
api_key: API key (optional)
Returns:
OrderService instance
"""
global _order_service_instance
if _order_service_instance is None:
_order_service_instance = OrderService(
external_api_url=external_api_url,
api_key=api_key
)
return _order_service_instance

View File

@@ -1,17 +1,20 @@
from sqlalchemy.orm import Session
from typing import List, Optional
import uuid
import secrets
import hashlib
import datetime import datetime
from fastapi import HTTPException, status import hashlib
import secrets
import uuid
from os import getenv
from typing import List, Optional
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.error_codes import BizCode from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException, PermissionDeniedException from app.core.exceptions import BusinessException, PermissionDeniedException
from app.models.tenant_model import Tenants from app.core.logging_config import get_business_logger
from app.models.user_model import User from app.models.user_model import User
from app.models.app_model import App from app.models.workspace_model import Workspace, WorkspaceRole, InviteStatus, WorkspaceMember
from app.models.end_user_model import EndUser from app.repositories import workspace_repository
from app.models.workspace_model import Workspace, WorkspaceRole, WorkspaceInvite, InviteStatus, WorkspaceMember from app.repositories.workspace_invite_repository import WorkspaceInviteRepository
from app.schemas.workspace_schema import ( from app.schemas.workspace_schema import (
WorkspaceCreate, WorkspaceCreate,
WorkspaceUpdate, WorkspaceUpdate,
@@ -21,15 +24,9 @@ from app.schemas.workspace_schema import (
InviteAcceptRequest, InviteAcceptRequest,
WorkspaceMemberUpdate WorkspaceMemberUpdate
) )
from app.repositories import workspace_repository
from app.repositories.workspace_invite_repository import WorkspaceInviteRepository
from app.core.logging_config import get_business_logger
from app.core.config import settings
from app.services import user_service
from os import getenv
# 获取业务逻辑专用日志器 # 获取业务逻辑专用日志器
business_logger = get_business_logger() business_logger = get_business_logger()
import os #
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
def switch_workspace( def switch_workspace(
@@ -802,3 +799,4 @@ def get_workspace_models_configs(
f"llm={configs.get('llm')}, embedding={configs.get('embedding')}, rerank={configs.get('rerank')}" f"llm={configs.get('llm')}, embedding={configs.get('embedding')}, rerank={configs.get('rerank')}"
) )
return configs return configs