diff --git a/ai_service/llm/__init__.py b/ai_service/llm/__init__.py new file mode 100644 index 0000000..059fdd8 --- /dev/null +++ b/ai_service/llm/__init__.py @@ -0,0 +1,50 @@ +import os +from enum import Enum + +from pydantic import SecretStr + + +class ProviderEnum(str, Enum): + """支持的 LLM 服务商""" + DEEPSEEK = "deepseek" + OPENAI = "openai" + TONGYI = "tongyi" + +class LLMFactory(object): + + @staticmethod + def get_llm(provider: ProviderEnum, model: str = None, **kwargs): + if provider == ProviderEnum.DEEPSEEK: + from langchain_deepseek import ChatDeepSeek + api_key = os.getenv("DEEPSEEK_API_KEY") + model = model or "deepseek-chat" + return ChatDeepSeek( + api_key=SecretStr(api_key), + model=model, + streaming=True, + **kwargs + ) + + elif provider == ProviderEnum.OPENAI: + from langchain_openai import ChatOpenAI + api_key = os.getenv("OPENAI_API_KEY") + model = model or "gpt-3.5-turbo" + return ChatOpenAI( + api_key=SecretStr(api_key), + model=model, + streaming=True, + **kwargs + ) + + elif provider == ProviderEnum.TONGYI: + from langchain_community.llms import Tongyi + api_key = os.getenv("DASHSCOPE_API_KEY") + model = model or "qwen-turbo" + return Tongyi( + api_key=SecretStr(api_key), + model=model, + streaming=True, + **kwargs + ) + else: + raise ValueError(f"不支持的 LLM 服务商: {provider}") diff --git a/ai_service/llm/adapter/__init__.py b/ai_service/llm/adapter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai_service/llm/adapter/deepseek.py b/ai_service/llm/adapter/deepseek.py new file mode 100644 index 0000000..615923e --- /dev/null +++ b/ai_service/llm/adapter/deepseek.py @@ -0,0 +1,17 @@ +from langchain_deepseek import ChatDeepSeek + +from llm.base import MultiModalAICapability + + +class DeepSeekAdapter(MultiModalAICapability): + def __init__(self, api_key, model, **kwargs): + + self.llm = ChatDeepSeek(api_key=api_key, model=model, streaming=True) + + async def chat(self, messages, **kwargs): + # 兼容 DeepSeek 的调用方式 + return await self.llm.ainvoke(messages) + + async def stream_chat(self, messages, **kwargs): + async for chunk in self.llm.astream(messages): + yield chunk \ No newline at end of file diff --git a/ai_service/llm/adapter/genai.py b/ai_service/llm/adapter/genai.py new file mode 100644 index 0000000..f16c710 --- /dev/null +++ b/ai_service/llm/adapter/genai.py @@ -0,0 +1,21 @@ +# 假设有 google genai sdk +# from google_genai import GenAI +from llm.base import MultiModalAICapability + + +class GoogleGenAIAdapter(MultiModalAICapability): + def __init__(self, api_key, model, **kwargs): + self.api_key = api_key + self.model = model + # self.llm = GenAI(api_key=api_key, model=model) + + async def chat(self, messages, **kwargs): + # return await self.llm.chat(messages) + raise NotImplementedError("Google GenAI chat未实现") + + async def stream_chat(self, messages, **kwargs): + # async for chunk in self.llm.stream_chat(messages): + # yield chunk + raise NotImplementedError("Google GenAI stream_chat未实现") + + # 其他能力同理 \ No newline at end of file diff --git a/ai_service/llm/adapter/openai.py b/ai_service/llm/adapter/openai.py new file mode 100644 index 0000000..1a3adb9 --- /dev/null +++ b/ai_service/llm/adapter/openai.py @@ -0,0 +1,25 @@ +from llm.base import MultiModalAICapability +from langchain_openai import ChatOpenAI +# from openai import OpenAI # 如需图片/音频/视频等API + +class OpenAIAdapter(MultiModalAICapability): + def __init__(self, api_key, model, **kwargs): + self.llm = ChatOpenAI(api_key=api_key, model=model, streaming=True) + self.api_key = api_key + + async def chat(self, messages, **kwargs): + return await self.llm.ainvoke(messages) + + async def stream_chat(self, messages, **kwargs): + async for chunk in self.llm.astream(messages): + yield chunk + + # 如需图片生成(DALL·E),可实现如下 + def create_image_task(self, prompt, **kwargs): + # 伪代码,需用 openai.Image.create + # import openai + # response = openai.Image.create(api_key=self.api_key, prompt=prompt, ...) + # return response + raise NotImplementedError("OpenAI 图片生成请用 openai.Image.create 实现") + + # 其他能力同理 \ No newline at end of file diff --git a/ai_service/llm/adapter/tongyi.py b/ai_service/llm/adapter/tongyi.py new file mode 100644 index 0000000..7d7af82 --- /dev/null +++ b/ai_service/llm/adapter/tongyi.py @@ -0,0 +1,51 @@ +from langchain_deepseek import ChatDeepSeek +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis +import os + +from llm.base import MultiModalAICapability + + +class TongYiAdapter(MultiModalAICapability): + def __init__(self, api_key, model, **kwargs): + self.api_key = api_key + self.model = model + self.llm = ChatDeepSeek(api_key=api_key, model=model, streaming=True) + + async def chat(self, messages, **kwargs): + # 兼容 DeepSeek 的调用方式 + return await self.llm.ainvoke(messages) + + async def stream_chat(self, messages, **kwargs): + async for chunk in self.llm.astream(messages): + yield chunk + + @staticmethod + def create_image_task(api_key, model, prompt: str, style='', size='1024*1024', n=1): + """创建异步图片生成任务""" + rsp = ImageSynthesis.async_call( + api_key=api_key, + model=model, + prompt=prompt, + n=n, + style=style, + size=size + ) + if rsp.status_code == HTTPStatus.OK: + return rsp + else: + raise Exception(f"Failed, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}") + + @staticmethod + def fetch_image_task_status(task): + """获取异步图片任务状态""" + status = ImageSynthesis.fetch(task) + if status.status_code == HTTPStatus.OK: + return status.output.task_status + else: + raise Exception(f"Failed, status_code: {status.status_code}, code: {status.code}, message: {status.message}") + + \ No newline at end of file diff --git a/ai_service/llm/base.py b/ai_service/llm/base.py new file mode 100644 index 0000000..8f82046 --- /dev/null +++ b/ai_service/llm/base.py @@ -0,0 +1,37 @@ +from abc import ABC + +class MultiModalAICapability(ABC): + # 对话能力 + async def chat(self, messages, **kwargs): + raise NotImplementedError("chat not supported by this provider") + + async def stream_chat(self, messages, **kwargs): + raise NotImplementedError("stream_chat not supported by this provider") + + # 图片生成能力 + def create_image_task(self, prompt, **kwargs): + raise NotImplementedError("image generation not supported by this provider") + + def fetch_image_task_status(self, task): + raise NotImplementedError("image task status not supported by this provider") + + def fetch_image_result(self, task): + raise NotImplementedError("image result not supported by this provider") + + # 视频生成能力 + def create_video_task(self, prompt, **kwargs): + raise NotImplementedError("video generation not supported by this provider") + + def fetch_video_task_status(self, task): + raise NotImplementedError("video task status not supported by this provider") + + def fetch_video_result(self, task): + raise NotImplementedError("video result not supported by this provider") + + # 知识库能力 + def query_knowledge(self, query, **kwargs): + raise NotImplementedError("knowledge query not supported by this provider") + + # 语音合成能力 + def synthesize_speech(self, text, **kwargs): + raise NotImplementedError("speech synthesis not supported by this provider") \ No newline at end of file diff --git a/ai_service/llm/factory.py b/ai_service/llm/factory.py new file mode 100644 index 0000000..0fce39f --- /dev/null +++ b/ai_service/llm/factory.py @@ -0,0 +1,34 @@ +from .adapter.deepseek import DeepSeekAdapter +from .adapter.genai import GoogleGenAIAdapter +from .adapter.openai import OpenAIAdapter +from .adapter.tongyi import TongYiAdapter + + +def get_adapter(provider, api_key, model, **kwargs): + if provider == 'deepseek': + return DeepSeekAdapter(api_key, model, **kwargs) + elif provider == 'tongyi': + return TongYiAdapter(api_key, model, **kwargs) + elif provider == 'openai': + return OpenAIAdapter(api_key, model, **kwargs) + elif provider == 'google-genai': + return GoogleGenAIAdapter(api_key, model, **kwargs) + else: + raise ValueError('不支持的服务商') + +# 使用示例 +# adapter = get_adapter('tongyi', api_key='xxx', model='wanx_v1') + +# 对话 +# try: +# result = await adapter.chat(messages) +# except NotImplementedError: +# print("该服务商不支持对话能力") + +# # 图片生成 +# try: +# task = adapter.create_image_task(prompt="一只猫") +# status = adapter.fetch_image_task_status(task) +# result = adapter.fetch_image_result(task) +# except NotImplementedError: +# print("该服务商不支持图片生成") \ No newline at end of file diff --git a/ai_service/models/ai.py b/ai_service/models/ai.py index 5c94dd3..edd82e4 100644 --- a/ai_service/models/ai.py +++ b/ai_service/models/ai.py @@ -1,5 +1,5 @@ from sqlalchemy import ( - Column, Integer, String, Text, DateTime, Boolean, Float, ForeignKey + Column, Integer, String, Text, DateTime, Boolean, Float, ForeignKey, JSON ) from sqlalchemy.orm import relationship, declarative_base @@ -250,4 +250,31 @@ class ChatRoleTool(Base): __tablename__ = 'ai_chat_role_tool' chat_role_id = Column(Integer, ForeignKey('ai_chat_role.id'), primary_key=True) - tool_id = Column(Integer, ForeignKey('ai_tool.id'), primary_key=True) \ No newline at end of file + tool_id = Column(Integer, ForeignKey('ai_tool.id'), primary_key=True) + + +class Image(CoreModel): + __tablename__ = 'ai_image' + + user_id = Column(Integer, ForeignKey('system_users.id'), nullable=True) + public_status = Column(Boolean, default=False, nullable=False) + + platform = Column(String(64), nullable=False) + model = Column(String(64), nullable=False) + + prompt = Column(Text(length=2000), nullable=False) + width = Column(Integer, nullable=False) + height = Column(Integer, nullable=False) + options = Column(JSON, nullable=True) + + status = Column(String(20), nullable=False) + pic_url = Column(String(2048), nullable=True) + error_message = Column(String(1024), nullable=True) + + task_id = Column(String(1024), nullable=True) + buttons = Column(String(2048), nullable=True) + + user = relationship("DjangoUser", backref="images") + + def __str__(self): + return f"Image #{self.id} ({self.prompt[:30]})" \ No newline at end of file diff --git a/backend/ai/migrations/0005_image.py b/backend/ai/migrations/0005_image.py new file mode 100644 index 0000000..86eaa54 --- /dev/null +++ b/backend/ai/migrations/0005_image.py @@ -0,0 +1,142 @@ +# Generated by Django 5.2.1 on 2025-07-21 03:06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ai", "0004_alter_chatconversation_model_id_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Image", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "remark", + models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + verbose_name="备注", + ), + ), + ( + "creator", + models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + verbose_name="创建人", + ), + ), + ( + "modifier", + models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + verbose_name="修改人", + ), + ), + ( + "update_time", + models.DateTimeField( + auto_now=True, + db_comment="修改时间", + help_text="修改时间", + null=True, + verbose_name="修改时间", + ), + ), + ( + "create_time", + models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + verbose_name="创建时间", + ), + ), + ( + "is_deleted", + models.BooleanField( + db_comment="是否软删除", + default=False, + verbose_name="是否软删除", + ), + ), + ( + "public_status", + models.BooleanField(default=False, verbose_name="是否发布"), + ), + ("platform", models.CharField(max_length=64, verbose_name="平台")), + ("model", models.CharField(max_length=64, verbose_name="模型")), + ("prompt", models.TextField(max_length=2000, verbose_name="提示词")), + ("width", models.IntegerField(verbose_name="图片宽度")), + ("height", models.IntegerField(verbose_name="图片高度")), + ("options", models.JSONField(null=True, verbose_name="绘制参数")), + ("status", models.CharField(max_length=20, verbose_name="绘画状态")), + ( + "pic_url", + models.URLField( + max_length=2048, null=True, verbose_name="图片地址" + ), + ), + ( + "error_message", + models.CharField( + max_length=1024, null=True, verbose_name="错误信息" + ), + ), + ( + "task_id", + models.CharField( + max_length=1024, null=True, verbose_name="任务编号" + ), + ), + ( + "buttons", + models.CharField( + max_length=2048, null=True, verbose_name="mj buttons 按钮" + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + db_comment="用户编号", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="用户", + ), + ), + ], + options={ + "verbose_name": "AI 绘画表", + "verbose_name_plural": "AI 绘画表", + "db_table": "ai_image", + }, + ), + ] diff --git a/backend/ai/models.py b/backend/ai/models.py index 3f4266a..53d0ea7 100644 --- a/backend/ai/models.py +++ b/backend/ai/models.py @@ -330,3 +330,35 @@ class ChatMessage(CoreModel): def __str__(self): return self.content[:30] + + +class Image(CoreModel): + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, blank=True, + on_delete=models.SET_NULL, + verbose_name="用户", + db_comment="用户编号" + ) + public_status = models.BooleanField(default=False, verbose_name='是否发布') + + platform = models.CharField(max_length=64, verbose_name='平台') + model = models.CharField(max_length=64, verbose_name='模型') + + prompt = models.TextField(max_length=2000, verbose_name='提示词') + width = models.IntegerField(verbose_name='图片宽度') + height = models.IntegerField(verbose_name='图片高度') + options = models.JSONField(null=True, verbose_name='绘制参数') + + status = models.CharField(max_length=20, verbose_name='绘画状态') + pic_url = models.URLField(max_length=2048, null=True, verbose_name='图片地址') + error_message = models.CharField(max_length=1024, null=True, verbose_name='错误信息') + + task_id = models.CharField(max_length=1024, null=True, verbose_name='任务编号') + buttons = models.CharField(max_length=2048, null=True, verbose_name='mj buttons 按钮') + + class Meta: + db_table = 'ai_image' + verbose_name = 'AI 绘画表' + verbose_name_plural = verbose_name diff --git a/web/apps/web-antd/src/views/ai/image/index.vue b/web/apps/web-antd/src/views/ai/image/index.vue new file mode 100644 index 0000000..1d68782 --- /dev/null +++ b/web/apps/web-antd/src/views/ai/image/index.vue @@ -0,0 +1,11 @@ + + + + +