feat: Add prompt template feature

pull/21676/head
baonudesifeizhai 11 months ago
parent 37e19de7ab
commit 75f232d832

28
.gitignore vendored

@ -214,3 +214,31 @@ mise.toml
# AI Assistant
.roo/
# Node.js
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
pnpm-lock.yaml
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
pip-cache/
.env
# IDEs
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

@ -266,3 +266,236 @@ To protect your privacy, please avoid posting security issues on GitHub. Instead
## License
This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions.
# Dify 多智能体记忆问题复现方案
## 问题概述
根据 [GitHub Issue #21640](https://github.com/langgenius/dify/issues/21640),在多智能体聊天流程系统中存在记忆隔离问题:
- 每个智能体都有记忆功能10条消息
- 当对话流程切换到不同智能体时,新智能体会看到来自其他智能体的聊天历史
- 这导致智能体产生"幻觉"hallucination回答不准确
## 复现方法
### 方法1手动复现推荐
1. **登录Dify控制台**
- 访问您的Dify实例
- 登录管理员账户
2. **创建工作流应用**
- 创建新的工作流应用
- 选择"工作流"模式
3. **添加节点**
```
Start节点 → 问题分类器 → Agent A (技术) / Agent B (商业) → End节点
```
4. **配置智能体**
- **Agent A**
- 系统提示词:"你是Agent A专门处理技术问题"
- 启用记忆窗口大小10
- **Agent B**
- 系统提示词:"你是Agent B专门处理商业问题"
- 启用记忆窗口大小10
5. **配置路由逻辑**
- 使用问题分类器根据内容类型路由
- 技术问题 → Agent A
- 商业问题 → Agent B
6. **测试场景**
```
用户: "如何配置Docker容器" → Agent A回答
用户: "什么是Kubernetes" → Agent A回答
用户: "如何制定营销策略?" → Agent B回答可能引用技术内容
用户: "如何提高客户满意度?" → Agent B回答可能引用技术内容
```
7. **观察问题**
- 检查Agent B的回答是否包含Docker、Kubernetes等技术相关内容
- 如果包含,说明存在记忆隔离问题
### 方法2使用复现脚本
1. **配置脚本**
```bash
# 编辑 reproduction_script.py
DIFY_BASE_URL = "http://your-dify-instance.com"
DIFY_API_KEY = "your_api_key_here"
```
2. **运行脚本**
```bash
python reproduction_script.py
```
3. **分析结果**
- 脚本会自动检测Agent B回答中的技术关键词
- 如果发现技术相关内容,说明存在记忆隔离问题
## 问题分析
### 当前实现机制
1. **记忆存储**所有节点共享同一个对话ID
2. **记忆获取**:基于整个对话获取历史消息
3. **记忆配置**:每个节点可独立配置,但都基于同一对话
### 核心问题
```python
# 当前记忆获取逻辑 (api/core/workflow/nodes/llm/llm_utils.py)
def fetch_memory(variable_pool, app_id, node_data_memory, model_instance):
# 获取对话ID
conversation_id = variable_pool.get(["sys", "CONVERSATION_ID"])
# 基于整个对话获取记忆 - 问题所在
conversation = session.scalar(stmt)
memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)
```
## 解决方案
### 方案1节点特定记忆推荐
1. **修改MemoryConfig**
```python
class MemoryConfig(BaseModel):
window: WindowConfig
role_prefix: Optional[RolePrefix] = None
query_prompt_template: Optional[str] = None
# 新增:节点特定记忆开关
node_specific_memory: bool = False
```
2. **修改记忆获取逻辑**
```python
def fetch_memory(variable_pool, app_id, node_data_memory, model_instance, node_id):
# 如果启用节点特定记忆,只获取该节点的历史
if node_data_memory and node_data_memory.node_specific_memory:
# 过滤该节点的消息历史
pass
```
3. **前端UI修改**
- 在记忆配置中添加"节点特定记忆"开关
- 提供用户友好的说明文字
### 方案2记忆过滤
1. **数据库层面过滤**
- 在Message表中添加node_id字段
- 根据节点ID过滤历史消息
2. **应用层面过滤**
- 在记忆获取时添加节点过滤条件
- 只包含特定节点参与的消息
### 方案3记忆上下文切换
1. **动态记忆管理**
- 在节点切换时调整记忆上下文
- 根据节点类型选择性地包含历史
## 实施步骤
### 后端修改
1. **修改数据模型**
```sql
-- 可选在Message表中添加node_id字段
ALTER TABLE messages ADD COLUMN node_id VARCHAR(255);
CREATE INDEX idx_messages_node_id ON messages(node_id);
```
2. **修改核心代码**
- `api/core/prompt/entities/advanced_prompt_entities.py`
- `api/core/memory/token_buffer_memory.py`
- `api/core/workflow/nodes/llm/llm_utils.py`
3. **添加测试用例**
- 单元测试验证记忆隔离功能
- 集成测试验证多智能体场景
### 前端修改
1. **UI组件更新**
- `web/app/components/workflow/nodes/_base/components/memory-config.tsx`
- 添加节点特定记忆开关
2. **国际化支持**
- 在 `web/i18n/zh-Hans/workflow.ts` 中添加相关文本
3. **类型定义更新**
- 更新TypeScript类型定义
## 测试验证
### 功能测试
1. **记忆隔离测试**
- 验证节点特定记忆开关是否正常工作
- 验证不同节点的记忆是否隔离
2. **多智能体测试**
- 测试技术Agent和商业Agent的记忆隔离
- 验证回答的准确性和相关性
### 性能测试
1. **记忆过滤性能**
- 验证记忆过滤对性能的影响
- 确保在大规模对话中的稳定性
2. **数据库查询优化**
- 验证节点过滤查询的性能
- 确保索引的有效性
### 用户体验测试
1. **功能易用性**
- 验证用户是否能够理解和使用新功能
- 确保配置界面的友好性
2. **向后兼容性**
- 验证现有工作流是否正常工作
- 确保默认行为保持不变
## 预期效果
### 问题解决
1. **消除幻觉**Agent B不再看到Agent A的技术对话历史
2. **提高准确性**:每个智能体只基于自己的专业领域回答
3. **改善用户体验**:多智能体对话更加准确和连贯
### 功能增强
1. **灵活配置**:用户可以选择是否启用节点特定记忆
2. **向后兼容**:现有工作流无需修改即可正常工作
3. **性能优化**:通过记忆过滤减少不必要的上下文
## 相关文件
- `reproduction_steps.md` - 详细的复现步骤
- `implementation_example.py` - 实现示例代码
- `reproduction_script.py` - 自动化复现脚本
## 贡献指南
如果您想为这个功能做出贡献:
1. Fork Dify仓库
2. 创建功能分支
3. 实现节点特定记忆功能
4. 添加测试用例
5. 提交Pull Request
## 联系方式
- GitHub Issue: [#21640](https://github.com/langgenius/dify/issues/21640)
- 项目主页: [https://github.com/langgenius/dify](https://github.com/langgenius/dify)

@ -43,7 +43,7 @@ api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm"
api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies")
# Import other controllers
from . import admin, apikey, extension, feature, ping, setup, version
from . import admin, apikey, extension, feature, ping, prompt_template, setup, version
# Import app controllers
from .app import (

@ -0,0 +1,84 @@
from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
from libs.login import login_required
from services.prompt_template_service import PromptTemplateService
class PromptTemplateListApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
templates = PromptTemplateService.get_prompt_templates()
# Manual serialization is no longer needed with Flask-RESTful
return [template.to_dict() for template in templates]
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('name', type=str, required=True, help='Name is required')
parser.add_argument('mode', type=str, required=True, help='Mode is required')
parser.add_argument('prompt_content', type=str, required=True, help='Prompt content is required.')
parser.add_argument('description', type=str, required=False)
parser.add_argument('tags', type=list, location='json')
parser.add_argument('model_name', type=str, required=False, location='json')
parser.add_argument('model_parameters', type=dict, required=False, location='json')
args = parser.parse_args()
template = PromptTemplateService.create_prompt_template(**args)
return template.to_dict(), 201
api.add_resource(PromptTemplateListApi, '/prompt-templates')
class PromptTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, template_id: str):
"""
Get a single prompt template.
"""
template = PromptTemplateService.get_prompt_template(template_id)
return template.to_dict()
@setup_required
@login_required
@account_initialization_required
def put(self, template_id: str):
"""
Update a prompt template.
"""
parser = reqparse.RequestParser()
parser.add_argument('name', type=str, required=True, help='Name is required')
parser.add_argument('mode', type=str, required=True, help='Mode is required')
parser.add_argument('prompt_content', type=str, required=True, help='Prompt content is required')
parser.add_argument('description', type=str, required=False)
parser.add_argument('tags', type=list, location='json')
parser.add_argument('model_name', type=str, required=False, location='json')
parser.add_argument('model_parameters', type=dict, required=False, location='json')
args = parser.parse_args()
template = PromptTemplateService.update_prompt_template(template_id=template_id, **args)
return template.to_dict()
@setup_required
@login_required
@account_initialization_required
def delete(self, template_id: str):
"""
Delete a prompt template.
"""
try:
PromptTemplateService.delete_prompt_template(template_id)
except NotFound:
# According to REST principles, DELETE should be idempotent.
# If the resource is already gone, we can consider the operation successful.
pass
return '', 204
api.add_resource(PromptTemplateApi, '/prompt-templates/<uuid:template_id>')

@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

@ -60,6 +60,7 @@ class LLMNodeCompletionModelPromptTemplate(CompletionModelPromptTemplate):
class LLMNodeData(BaseNodeData):
model: ModelConfig
prompt_template_id: Optional[str] = None
prompt_template: Sequence[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate
prompt_config: PromptConfig = Field(default_factory=PromptConfig)
memory: Optional[MemoryConfig] = None

@ -68,6 +68,7 @@ from core.workflow.nodes.event import (
RunStreamChunkEvent,
)
from core.workflow.utils.variable_template_parser import VariableTemplateParser
from services.prompt_template_service import PromptTemplateService
from . import llm_utils
from .entities import (
@ -151,6 +152,28 @@ class LLMNode(BaseNode[LLMNodeData]):
variable_pool = self.graph_runtime_state.variable_pool
try:
# If a prompt template is used, load it and override node data
if self.node_data.prompt_template_id:
prompt_template_entity = PromptTemplateService.get_prompt_template_for_workflow(
template_id=self.node_data.prompt_template_id,
tenant_id=self.tenant_id
)
latest_version = prompt_template_entity.get_latest_version()
if latest_version:
# Override prompt_template based on its type
if isinstance(self.node_data.prompt_template, LLMNodeCompletionModelPromptTemplate):
self.node_data.prompt_template.text = latest_version.prompt_text
elif isinstance(self.node_data.prompt_template, list) and self.node_data.prompt_template:
# For chat mode, typically we modify the last user message or a specific system message.
# Here, for simplicity, we'll assume the main prompt is the first message.
self.node_data.prompt_template[0].text = latest_version.prompt_text
# Override model settings
if latest_version.model_name and latest_version.model_parameters:
self.node_data.model.name = latest_version.model_name
self.node_data.model.completion_params = latest_version.model_parameters
# init messages template
self.node_data.prompt_template = self._transform_chat_messages(self.node_data.prompt_template)

@ -0,0 +1,66 @@
"""feat: add prompt management tables
Revision ID: e3845da34e79
Revises: 0ab65e1cc7fa
Create Date: 2025-06-27 20:08:44.738741
"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'e3845da34e79'
down_revision = '0ab65e1cc7fa'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('prompt_templates',
sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('mode', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('tags', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
sa.PrimaryKeyConstraint('id', name='prompt_template_pkey')
)
with op.batch_alter_table('prompt_templates', schema=None) as batch_op:
batch_op.create_index('prompt_template_tenant_id_idx', ['tenant_id'], unique=False)
op.create_table('prompt_versions',
sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('prompt_template_id', sa.UUID(), nullable=False),
sa.Column('version', sa.String(length=255), nullable=False),
sa.Column('prompt_text', sa.Text(), nullable=False),
sa.Column('variables', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('model_settings', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', sa.String(length=50), nullable=False),
sa.Column('created_by', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
sa.ForeignKeyConstraint(['prompt_template_id'], ['prompt_templates.id'], name=op.f('prompt_versions_prompt_template_id_fkey')),
sa.PrimaryKeyConstraint('id', name='prompt_version_pkey')
)
with op.batch_alter_table('prompt_versions', schema=None) as batch_op:
batch_op.create_index('prompt_version_template_id_idx', ['prompt_template_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('prompt_versions', schema=None) as batch_op:
batch_op.drop_index('prompt_version_template_id_idx')
op.drop_table('prompt_versions')
with op.batch_alter_table('prompt_templates', schema=None) as batch_op:
batch_op.drop_index('prompt_template_tenant_id_idx')
op.drop_table('prompt_templates')
# ### end Alembic commands ###

@ -0,0 +1,35 @@
"""add model_name and model_parameters to prompt_version
Revision ID: 090bb5fe6078
Revises: e3845da34e79
Create Date: 2025-06-28 16:07:29.626618
"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '090bb5fe6078'
down_revision = 'e3845da34e79'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('prompt_versions', schema=None) as batch_op:
batch_op.add_column(sa.Column('model_name', sa.String(length=255), nullable=True))
batch_op.add_column(sa.Column('model_parameters', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('prompt_versions', schema=None) as batch_op:
batch_op.drop_column('model_parameters')
batch_op.drop_column('model_name')
# ### end Alembic commands ###

@ -56,6 +56,7 @@ from .model import (
TraceAppConfig,
UploadFile,
)
from .prompt_template import PromptTemplate, PromptVersion
from .provider import (
LoadBalancingModelConfig,
Provider,
@ -140,6 +141,8 @@ __all__ = [
"MessageFile",
"OperationLog",
"PinnedConversation",
"PromptTemplate",
"PromptVersion",
"Provider",
"ProviderModel",
"ProviderModelSetting",

@ -0,0 +1,96 @@
from __future__ import annotations
from sqlalchemy import desc
from sqlalchemy.dialects.postgresql import JSONB, UUID
from .engine import db
# from .account import Account <- This is the source of the circular import
class PromptTemplate(db.Model):
__tablename__ = 'prompt_templates'
__table_args__ = (
db.PrimaryKeyConstraint('id', name='prompt_template_pkey'),
db.Index('prompt_template_tenant_id_idx', 'tenant_id'),
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
tenant_id = db.Column(UUID, nullable=False)
name = db.Column(db.String(255), nullable=False)
mode = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=True)
tags = db.Column(JSONB, nullable=True)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_at = db.Column(db.DateTime, nullable=False,
server_default=db.text('CURRENT_TIMESTAMP(0)'),
onupdate=db.text('CURRENT_TIMESTAMP(0)'))
# Relationships
versions = db.relationship('PromptVersion', back_populates='prompt_template',
cascade="all, delete-orphan", lazy='dynamic')
def get_latest_version(self):
"""
Fetches the most recent version of the prompt template.
"""
return self.versions.order_by(desc(PromptVersion.created_at)).first()
def to_dict(self):
latest_version = self.get_latest_version()
model_settings = None
if latest_version:
model_settings = {
"model_name": latest_version.model_name,
"parameters": latest_version.model_parameters
}
return {
'id': str(self.id),
'name': self.name,
'mode': self.mode,
'description': self.description,
'tags': self.tags,
'prompt_content': latest_version.prompt_text if latest_version else None,
'model_settings': model_settings,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
}
def __repr__(self):
return f'<PromptTemplate @{self.id}>'
class PromptVersion(db.Model):
__tablename__ = 'prompt_versions'
__table_args__ = (
db.PrimaryKeyConstraint('id', name='prompt_version_pkey'),
db.Index('prompt_version_template_id_idx', 'prompt_template_id'),
)
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
prompt_template_id = db.Column(UUID, db.ForeignKey('prompt_templates.id'), nullable=False)
version = db.Column(db.String(255), nullable=False, default='1.0')
prompt_text = db.Column(db.Text, nullable=False)
variables = db.Column(JSONB, nullable=True)
model_name = db.Column(db.String(255), nullable=True)
model_parameters = db.Column(JSONB, nullable=True)
model_settings = db.Column(JSONB, nullable=True)
status = db.Column(db.String(50), nullable=False, default='draft') # e.g., draft, published, archived
created_by = db.Column(UUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_at = db.Column(db.DateTime, nullable=False,
server_default=db.text('CURRENT_TIMESTAMP(0)'),
onupdate=db.text('CURRENT_TIMESTAMP(0)'))
# Relationships
prompt_template = db.relationship('PromptTemplate', back_populates='versions')
def __repr__(self):
return f'<PromptVersion @{self.id} for Template {self.prompt_template_id}>'

@ -0,0 +1,4 @@
from app import create_app
app = create_app()
print(app.url_map)

@ -0,0 +1,163 @@
import logging
from typing import Optional
from uuid import UUID
from flask_login import current_user
from werkzeug.exceptions import NotFound
from models import db
from models.prompt_template import PromptTemplate, PromptVersion
class PromptTemplateService:
@staticmethod
def get_prompt_templates():
"""
Get all prompt templates for the current tenant.
"""
if not current_user.is_authenticated:
raise NotFound("User not authenticated.")
return db.session.query(PromptTemplate).filter_by(tenant_id=current_user.current_tenant_id).all()
@staticmethod
def create_prompt_template(
name: str,
mode: str,
prompt_content: str,
description: Optional[str] = None,
tags: Optional[list] = None,
model_name: Optional[str] = None,
model_parameters: Optional[dict] = None,
):
"""
Create a new prompt template and its initial version.
"""
logging.info(f"Attempting to create prompt template '{name}' for tenant {current_user.current_tenant_id}")
try:
if not current_user.is_authenticated:
logging.error("User not authenticated during prompt template creation.")
raise NotFound("User not authenticated.")
template = PromptTemplate(
name=name,
mode=mode,
description=description,
tags=tags,
tenant_id=current_user.current_tenant_id,
)
initial_version = PromptVersion(
prompt_template=template,
prompt_text=prompt_content,
model_name=model_name,
model_parameters=model_parameters,
created_by=UUID(current_user.id),
)
db.session.add(template)
db.session.add(initial_version)
db.session.commit()
logging.info(f"Successfully created prompt template with id {template.id}")
return template
except Exception as e:
logging.error(f"Error creating prompt template '{name}': {e}", exc_info=True)
db.session.rollback()
raise
@staticmethod
def get_prompt_template(template_id: str):
"""
Get a specific prompt template by ID for the current tenant.
"""
if not current_user.is_authenticated:
raise NotFound("User not authenticated.")
template = (
db.session.query(PromptTemplate)
.filter_by(id=template_id, tenant_id=current_user.current_tenant_id)
.first()
)
if not template:
raise NotFound(f"Prompt template with id {template_id} not found.")
return template
@staticmethod
def get_prompt_template_for_workflow(template_id: str, tenant_id: str):
"""
Get a specific prompt template by ID for a specific tenant.
This method is designed for workflow context where current_user is not available.
"""
template = (
db.session.query(PromptTemplate)
.filter_by(id=template_id, tenant_id=tenant_id)
.first()
)
if not template:
raise NotFound(f"Prompt template with id {template_id} not found for the given tenant.")
return template
@staticmethod
def update_prompt_template(
template_id: str,
name: str,
mode: str,
prompt_content: str,
description: Optional[str] = None,
tags: Optional[list] = None,
model_name: Optional[str] = None,
model_parameters: Optional[dict] = None,
):
"""
Update a prompt template and create a new version if content changes.
"""
if not current_user.is_authenticated:
raise NotFound("User not authenticated.")
template = PromptTemplateService.get_prompt_template(template_id)
if not template:
raise NotFound(f"Prompt template with id {template_id} not found.")
# Update the main template fields
template.name = name
template.mode = mode
template.description = description
template.tags = tags
latest_version = template.get_latest_version()
# Create a new version only if the prompt content or model settings have changed
if (latest_version.prompt_text != prompt_content or
latest_version.model_name != model_name or
latest_version.model_parameters != model_parameters):
new_version = PromptVersion(
prompt_template=template,
prompt_text=prompt_content,
model_name=model_name,
model_parameters=model_parameters,
created_by=UUID(current_user.id),
)
db.session.add(new_version)
db.session.commit()
return template
@staticmethod
def delete_prompt_template(template_id: str):
"""
Delete a specific prompt template by ID for the current tenant.
"""
if not current_user.is_authenticated:
raise NotFound("User not authenticated.")
template = (
db.session.query(PromptTemplate)
.filter_by(id=template_id, tenant_id=current_user.current_tenant_id)
.first()
)
if not template:
raise NotFound(f"Prompt template with id {template_id} not found.")
db.session.delete(template)
db.session.commit()

@ -1,17 +0,0 @@
<clickhouse>
<users>
<default>
<password></password>
<networks>
<ip>::1</ip> <!-- change to ::/0 to allow access from all addresses -->
<ip>127.0.0.1</ip>
<ip>10.0.0.0/8</ip>
<ip>172.16.0.0/12</ip>
<ip>192.168.0.0/16</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
<access_management>1</access_management>
</default>
</users>
</clickhouse>

@ -1 +0,0 @@
ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30;

@ -1,222 +0,0 @@
---
# Copyright OpenSearch Contributors
# SPDX-License-Identifier: Apache-2.0
# Description:
# Default configuration for OpenSearch Dashboards
# OpenSearch Dashboards is served by a back end server. This setting specifies the port to use.
# server.port: 5601
# Specifies the address to which the OpenSearch Dashboards server will bind. IP addresses and host names are both valid values.
# The default is 'localhost', which usually means remote machines will not be able to connect.
# To allow connections from remote users, set this parameter to a non-loopback address.
# server.host: "localhost"
# Enables you to specify a path to mount OpenSearch Dashboards at if you are running behind a proxy.
# Use the `server.rewriteBasePath` setting to tell OpenSearch Dashboards if it should remove the basePath
# from requests it receives, and to prevent a deprecation warning at startup.
# This setting cannot end in a slash.
# server.basePath: ""
# Specifies whether OpenSearch Dashboards should rewrite requests that are prefixed with
# `server.basePath` or require that they are rewritten by your reverse proxy.
# server.rewriteBasePath: false
# The maximum payload size in bytes for incoming server requests.
# server.maxPayloadBytes: 1048576
# The OpenSearch Dashboards server's name. This is used for display purposes.
# server.name: "your-hostname"
# The URLs of the OpenSearch instances to use for all your queries.
# opensearch.hosts: ["http://localhost:9200"]
# OpenSearch Dashboards uses an index in OpenSearch to store saved searches, visualizations and
# dashboards. OpenSearch Dashboards creates a new index if the index doesn't already exist.
# opensearchDashboards.index: ".opensearch_dashboards"
# The default application to load.
# opensearchDashboards.defaultAppId: "home"
# Setting for an optimized healthcheck that only uses the local OpenSearch node to do Dashboards healthcheck.
# This settings should be used for large clusters or for clusters with ingest heavy nodes.
# It allows Dashboards to only healthcheck using the local OpenSearch node rather than fan out requests across all nodes.
#
# It requires the user to create an OpenSearch node attribute with the same name as the value used in the setting
# This node attribute should assign all nodes of the same cluster an integer value that increments with each new cluster that is spun up
# e.g. in opensearch.yml file you would set the value to a setting using node.attr.cluster_id:
# Should only be enabled if there is a corresponding node attribute created in your OpenSearch config that matches the value here
# opensearch.optimizedHealthcheckId: "cluster_id"
# If your OpenSearch is protected with basic authentication, these settings provide
# the username and password that the OpenSearch Dashboards server uses to perform maintenance on the OpenSearch Dashboards
# index at startup. Your OpenSearch Dashboards users still need to authenticate with OpenSearch, which
# is proxied through the OpenSearch Dashboards server.
# opensearch.username: "opensearch_dashboards_system"
# opensearch.password: "pass"
# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively.
# These settings enable SSL for outgoing requests from the OpenSearch Dashboards server to the browser.
# server.ssl.enabled: false
# server.ssl.certificate: /path/to/your/server.crt
# server.ssl.key: /path/to/your/server.key
# Optional settings that provide the paths to the PEM-format SSL certificate and key files.
# These files are used to verify the identity of OpenSearch Dashboards to OpenSearch and are required when
# xpack.security.http.ssl.client_authentication in OpenSearch is set to required.
# opensearch.ssl.certificate: /path/to/your/client.crt
# opensearch.ssl.key: /path/to/your/client.key
# Optional setting that enables you to specify a path to the PEM file for the certificate
# authority for your OpenSearch instance.
# opensearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ]
# To disregard the validity of SSL certificates, change this setting's value to 'none'.
# opensearch.ssl.verificationMode: full
# Time in milliseconds to wait for OpenSearch to respond to pings. Defaults to the value of
# the opensearch.requestTimeout setting.
# opensearch.pingTimeout: 1500
# Time in milliseconds to wait for responses from the back end or OpenSearch. This value
# must be a positive integer.
# opensearch.requestTimeout: 30000
# List of OpenSearch Dashboards client-side headers to send to OpenSearch. To send *no* client-side
# headers, set this value to [] (an empty list).
# opensearch.requestHeadersWhitelist: [ authorization ]
# Header names and values that are sent to OpenSearch. Any custom headers cannot be overwritten
# by client-side headers, regardless of the opensearch.requestHeadersWhitelist configuration.
# opensearch.customHeaders: {}
# Time in milliseconds for OpenSearch to wait for responses from shards. Set to 0 to disable.
# opensearch.shardTimeout: 30000
# Logs queries sent to OpenSearch. Requires logging.verbose set to true.
# opensearch.logQueries: false
# Specifies the path where OpenSearch Dashboards creates the process ID file.
# pid.file: /var/run/opensearchDashboards.pid
# Enables you to specify a file where OpenSearch Dashboards stores log output.
# logging.dest: stdout
# Set the value of this setting to true to suppress all logging output.
# logging.silent: false
# Set the value of this setting to true to suppress all logging output other than error messages.
# logging.quiet: false
# Set the value of this setting to true to log all events, including system usage information
# and all requests.
# logging.verbose: false
# Set the interval in milliseconds to sample system and process performance
# metrics. Minimum is 100ms. Defaults to 5000.
# ops.interval: 5000
# Specifies locale to be used for all localizable strings, dates and number formats.
# Supported languages are the following: English - en , by default , Chinese - zh-CN .
# i18n.locale: "en"
# Set the allowlist to check input graphite Url. Allowlist is the default check list.
# vis_type_timeline.graphiteAllowedUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite']
# Set the blocklist to check input graphite Url. Blocklist is an IP list.
# Below is an example for reference
# vis_type_timeline.graphiteBlockedIPs: [
# //Loopback
# '127.0.0.0/8',
# '::1/128',
# //Link-local Address for IPv6
# 'fe80::/10',
# //Private IP address for IPv4
# '10.0.0.0/8',
# '172.16.0.0/12',
# '192.168.0.0/16',
# //Unique local address (ULA)
# 'fc00::/7',
# //Reserved IP address
# '0.0.0.0/8',
# '100.64.0.0/10',
# '192.0.0.0/24',
# '192.0.2.0/24',
# '198.18.0.0/15',
# '192.88.99.0/24',
# '198.51.100.0/24',
# '203.0.113.0/24',
# '224.0.0.0/4',
# '240.0.0.0/4',
# '255.255.255.255/32',
# '::/128',
# '2001:db8::/32',
# 'ff00::/8',
# ]
# vis_type_timeline.graphiteBlockedIPs: []
# opensearchDashboards.branding:
# logo:
# defaultUrl: ""
# darkModeUrl: ""
# mark:
# defaultUrl: ""
# darkModeUrl: ""
# loadingLogo:
# defaultUrl: ""
# darkModeUrl: ""
# faviconUrl: ""
# applicationTitle: ""
# Set the value of this setting to true to capture region blocked warnings and errors
# for your map rendering services.
# map.showRegionBlockedWarning: false%
# Set the value of this setting to false to suppress search usage telemetry
# for reducing the load of OpenSearch cluster.
# data.search.usageTelemetry.enabled: false
# 2.4 renames 'wizard.enabled: false' to 'vis_builder.enabled: false'
# Set the value of this setting to false to disable VisBuilder
# functionality in Visualization.
# vis_builder.enabled: false
# 2.4 New Experimental Feature
# Set the value of this setting to true to enable the experimental multiple data source
# support feature. Use with caution.
# data_source.enabled: false
# Set the value of these settings to customize crypto materials to encryption saved credentials
# in data sources.
# data_source.encryption.wrappingKeyName: 'changeme'
# data_source.encryption.wrappingKeyNamespace: 'changeme'
# data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
# 2.6 New ML Commons Dashboards Feature
# Set the value of this setting to true to enable the ml commons dashboards
# ml_commons_dashboards.enabled: false
# 2.12 New experimental Assistant Dashboards Feature
# Set the value of this setting to true to enable the assistant dashboards
# assistant.chat.enabled: false
# 2.13 New Query Assistant Feature
# Set the value of this setting to false to disable the query assistant
# observability.query_assist.enabled: false
# 2.14 Enable Ui Metric Collectors in Usage Collector
# Set the value of this setting to true to enable UI Metric collections
# usageCollection.uiMetric.enabled: false
opensearch.hosts: [https://localhost:9200]
opensearch.ssl.verificationMode: none
opensearch.username: admin
opensearch.password: 'Qazwsxedc!@#123'
opensearch.requestHeadersWhitelist: [authorization, securitytenant]
opensearch_security.multitenancy.enabled: true
opensearch_security.multitenancy.tenants.preferred: [Private, Global]
opensearch_security.readonly_mode.roles: [kibana_read_only]
# Use this setting if you are running opensearch-dashboards without https
opensearch_security.cookie.secure: false
server.host: '0.0.0.0'

@ -1,14 +0,0 @@
app:
port: 8194
debug: True
key: dify-sandbox
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /usr/local/bin/python3
enable_network: True # please make sure there is no network risk in your environment
allowed_syscalls: # please leave it empty if you have no idea how seccomp works
proxy:
socks5: ''
http: ''
https: ''

@ -1,35 +0,0 @@
app:
port: 8194
debug: True
key: dify-sandbox
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /usr/local/bin/python3
python_lib_path:
- /usr/local/lib/python3.10
- /usr/lib/python3.10
- /usr/lib/python3
- /usr/lib/x86_64-linux-gnu
- /etc/ssl/certs/ca-certificates.crt
- /etc/nsswitch.conf
- /etc/hosts
- /etc/resolv.conf
- /run/systemd/resolve/stub-resolv.conf
- /run/resolvconf/resolv.conf
- /etc/localtime
- /usr/share/zoneinfo
- /etc/timezone
# add more paths if needed
python_pip_mirror_url: https://pypi.tuna.tsinghua.edu.cn/simple
nodejs_path: /usr/local/bin/node
enable_network: True
allowed_syscalls:
- 1
- 2
- 3
# add all the syscalls which you require
proxy:
socks5: ''
http: ''
https: ''

@ -0,0 +1,5 @@
{
"dependencies": {
"date-fns": "^4.1.0"
}
}

@ -0,0 +1,95 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { fetchPromptTemplates, deletePromptTemplate } from '@/service/prompt-template'
import type { PromptTemplate } from '@/models/prompt-template'
import Button from '@/app/components/base/button'
import NewTemplateModal from './NewTemplateModal'
import { TrashIcon } from '@heroicons/react/24/outline'
const Container = () => {
const router = useRouter()
const [templates, setTemplates] = useState<PromptTemplate[]>([])
const [showNewModal, setShowNewModal] = useState(false)
const fetchTemplates = useCallback(() => {
fetchPromptTemplates().then(res => {
// res may be an array or an object with a data property
const templateList = Array.isArray(res) ? res : res.data
setTemplates(templateList || [])
})
}, [])
useEffect(() => {
fetchTemplates()
}, [fetchTemplates])
const handleSuccess = () => {
fetchTemplates()
}
const handleDelete = async (e: React.MouseEvent, id: string) => {
e.stopPropagation() // Prevent row click event
if (window.confirm('Are you sure you want to delete this template?')) {
try {
await deletePromptTemplate(id)
fetchTemplates()
} catch (e: any) {
alert(`Failed to delete template: ${e.message}`)
}
}
}
const handleRowClick = (id: string) => {
router.push(`/prompt-templates/${id}`)
}
return (
<>
<div className="p-4 sm:p-6 md:p-8">
<div className="flex justify-between items-center mb-4">
<h1 className="text-xl font-semibold">Prompt Templates</h1>
<Button variant="primary" onClick={() => router.push('/prompt-templates/new')}>Create Template</Button>
</div>
<div className="bg-white shadow-sm rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Model</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{templates.map((template) => (
<tr key={template.id} onClick={() => handleRowClick(template.id)} className="cursor-pointer hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{template.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{template.model_settings?.model_name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{new Date(template.created_at).toLocaleString()}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onClick={(e) => handleDelete(e, template.id)} className="text-red-600 hover:text-red-900 p-1">
<TrashIcon className="h-5 w-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{showNewModal && (
<NewTemplateModal
isShow={showNewModal}
onClose={() => setShowNewModal(false)}
onSuccess={handleSuccess}
/>
)}
</>
)
}
export default Container

@ -0,0 +1,187 @@
'use client'
import React, { useState } from 'react'
import type { PromptTemplate, PromptTemplateRequest } from '@/models/prompt-template'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import Button from '@/app/components/base/button'
import TagInput from '@/app/components/base/tag-input'
import Select from '@/app/components/base/select'
import { format as formatDate } from 'date-fns'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Trigger from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
type FormProps = {
type: 'create' | 'edit'
template?: PromptTemplate
templateId?: string
onSave: (data: PromptTemplateRequest) => void
onCancel: () => void
}
const Form = ({ type, template, templateId, onSave, onCancel }: FormProps) => {
const [formData, setFormData] = useState({
name: template?.name || '',
description: template?.description || '',
tags: template?.tags || [],
prompt_content: template?.prompt_content || '',
mode: template?.mode || 'completion',
model_name: template?.model_settings?.model_name || 'gpt-3.5-turbo',
provider: 'openai', // hardcode for now
model_parameters: template?.model_settings?.parameters || { temperature: 0.7 },
})
const [open, setOpen] = useState(false)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const handleSelectChange = (item: {value: string | number}) => {
setFormData(prev => ({ ...prev, mode: item.value as string }))
}
const handleTagsChange = (tags: string[]) => {
setFormData(prev => ({ ...prev, tags }))
}
const handleModelChange = (model: { modelId: string; provider: string; }) => {
setFormData(prev => ({
...prev,
model_name: model.modelId,
provider: model.provider,
model_parameters: {},
}))
}
const handleModelParamsChange = (params: FormValue) => {
setFormData(prev => ({ ...prev, model_parameters: params }))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const { provider, ...restData } = formData
onSave(restData as PromptTemplateRequest)
}
const title = type === 'create' ? 'Create Prompt Template' : 'Edit Prompt Template'
return (
<div className="p-8">
<h1 className="text-2xl font-semibold mb-6">{title}</h1>
<form onSubmit={handleSubmit} className="space-y-6">
{type === 'edit' && (
<div>
<label className="block text-sm font-medium text-gray-700">ID</label>
<p className="mt-1 text-sm text-gray-500">{templateId}</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700">Name</label>
<Input
name="name"
value={formData.name}
onChange={handleInputChange}
className="mt-1 block w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Mode</label>
<Select
className="mt-1"
defaultValue={formData.mode}
onSelect={handleSelectChange}
items={[
{ value: 'completion', name: 'Completion' },
{ value: 'chat', name: 'Chat' },
]}
allowSearch={false}
/>
</div>
<div className=''>
<label className="block text-sm font-medium text-gray-700 mb-1">Model</label>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} className='block'>
<Trigger
providerName={formData.provider}
modelId={formData.model_name}
disabled={false}
/>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<ModelParameterModal
isAdvancedMode={true}
mode={formData.mode}
provider={formData.provider}
modelId={formData.model_name}
setModel={handleModelChange}
completionParams={formData.model_parameters}
onCompletionParamsChange={handleModelParamsChange}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Prompt Content</label>
<Textarea
name="prompt_content"
value={formData.prompt_content}
onChange={handleInputChange}
className="mt-1 block w-full"
rows={10}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Description</label>
<Input
name="description"
value={formData.description!}
onChange={handleInputChange}
className="mt-1 block w-full"
placeholder="Enter description"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Tags (comma-separated)</label>
<TagInput
items={formData.tags!}
onChange={handleTagsChange}
/>
</div>
{type === 'edit' && template && (
<div className="text-sm text-gray-500">
Created: {formatDate(new Date(template.created_at), 'M/d/yyyy, h:mm:ss a')}
<br />
Last Updated: {formatDate(new Date(template.updated_at), 'M/d/yyyy, h:mm:ss a')}
</div>
)}
<div className="flex justify-end space-x-2">
<Button type="button" onClick={onCancel}>Cancel</Button>
<Button type="submit" variant="primary">Save Changes</Button>
</div>
</form>
</div>
)
}
export default Form

@ -0,0 +1,93 @@
'use client'
import { useState } from 'react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { createPromptTemplate } from '@/service/prompt-template'
import type { PromptTemplateRequest } from '@/models/prompt-template'
type NewTemplateModalProps = {
isShow: boolean
onClose: () => void
onSuccess: () => void
}
const NewTemplateModal = ({ isShow, onClose, onSuccess }: NewTemplateModalProps) => {
const [name, setName] = useState('')
const [promptContent, setPromptContent] = useState('')
const [modelName, setModelName] = useState('gpt-3.5-turbo')
const [modelParameters, setModelParameters] = useState({ temperature: 0.7, max_tokens: 256 })
const handleCreate = async () => {
if (!name.trim() || !promptContent.trim()) {
// Basic validation
alert('Name and Prompt Content cannot be empty.')
return
}
const data: PromptTemplateRequest = {
name,
prompt_content: promptContent,
model_settings: {
model_name: modelName,
model_parameters: modelParameters,
},
}
try {
await createPromptTemplate(data)
onSuccess()
onClose()
} catch (e: any) {
alert(`Failed to create template: ${e.message}`)
}
}
return (
<Modal
isShow={isShow}
onClose={onClose}
title="Create New Prompt Template"
className="!w-[600px]"
>
<div className="p-6">
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Template Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="e.g. My Awesome Prompt"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Prompt Content</label>
<textarea
value={promptContent}
onChange={(e) => setPromptContent(e.target.value)}
rows={8}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Write your prompt here. Use {{variable_name}} for variables."
/>
</div>
{/* Simplified Model Settings for now */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Model (simplified)</label>
<p className='text-sm text-gray-500'>Using default model settings for now.</p>
<p className='text-xs text-gray-400'>Model: {modelName}</p>
<p className='text-xs text-gray-400'>Params: {JSON.stringify(modelParameters)}</p>
</div>
<div className="flex justify-end gap-2">
<Button onClick={onClose}>Cancel</Button>
<Button variant="primary" onClick={handleCreate}>Create</Button>
</div>
</div>
</Modal>
)
}
export default NewTemplateModal

@ -0,0 +1,73 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { getPromptTemplate, updatePromptTemplate } from '@/service/prompt-template'
import type { PromptTemplate } from '@/models/prompt-template'
import Form from '../Form'
import Toast from '@/app/components/base/toast'
const Container = ({ id }: { id: string }) => {
const { t } = useTranslation()
const router = useRouter()
const [template, setTemplate] = useState<PromptTemplate | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
(async () => {
try {
const data = await getPromptTemplate(id)
setTemplate(data)
} catch (e) {
// handle error
console.error(e)
} finally {
setLoading(false)
}
})()
}, [id])
const handleSave = async (data: any) => {
try {
await updatePromptTemplate(id, data)
Toast.notify({
type: 'success',
message: t('common.api.saved'),
})
} catch(e: any) {
Toast.notify({
type: 'error',
message: e.message || 'Failed to save template',
})
}
}
if (loading) {
return <div>Loading...</div>
}
if (!template) {
// TODO: better error/not found display
return <div>Template not found</div>
}
const handleCancel = () => {
router.back()
}
return (
<div className="flex flex-col h-full">
<div className="grow overflow-y-auto">
<Form
type="edit"
template={template}
templateId={id}
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
</div>
)
}
export default Container

@ -0,0 +1,11 @@
'use client'
import React from 'react'
import Container from './Container'
const Page = ({ params }: { params: { id: string } }) => {
return (
<Container id={params.id} />
)
}
export default Page

@ -0,0 +1,46 @@
'use client'
import React from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import Form from '../Form'
import { createPromptTemplate } from '@/service/prompt-template'
import Toast from '@/app/components/base/toast'
const Container = () => {
const { t } = useTranslation()
const router = useRouter()
const handleSave = async (data: any) => {
try {
await createPromptTemplate(data)
Toast.notify({
type: 'success',
message: t('common.api.saved'),
})
router.push('/prompt-templates')
} catch (e: any) {
Toast.notify({
type: 'error',
message: e.message || 'Failed to create template',
})
}
}
const handleCancel = () => {
router.back()
}
return (
<div className="flex flex-col h-full">
<div className="grow overflow-y-auto">
<Form
type="create"
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
</div>
)
}
export default Container

@ -0,0 +1,11 @@
'use client'
import React from 'react'
import Container from './Container'
const Page = () => {
return (
<Container />
)
}
export default Page

@ -0,0 +1,7 @@
import Container from './Container'
const PromptTemplates = () => {
return <Container />
}
export default PromptTemplates

@ -19,7 +19,7 @@ import PlanBadge from './plan-badge'
import LicenseNav from './license-env'
import { Plan } from '../billing/type'
import { useGlobalPublicStore } from '@/context/global-public-context'
import PromptTemplatesNav from './prompt-templates-nav'
const navClassName = `
flex items-center relative px-3 h-8 rounded-xl
font-medium text-sm
@ -101,6 +101,7 @@ const Header = () => {
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
<PromptTemplatesNav />
</div>
<div className='flex min-w-0 flex-[1] items-center justify-end pl-2 pr-3 min-[1280px]:pl-3'>
<EnvNav />

@ -0,0 +1,26 @@
'use client'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
import cn from 'classnames'
const PromptTemplatesNav = () => {
const selectedLayoutSegment = useSelectedLayoutSegment()
const isSelected = selectedLayoutSegment === 'prompt-templates'
return (
<Link
href="/prompt-templates"
className={cn(
'flex items-center mr-3 px-3 h-8 text-sm font-medium rounded-lg',
isSelected
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-200'
)}
>
Prompts
</Link>
)
}
export default PromptTemplatesNav

@ -0,0 +1,24 @@
export type PromptTemplate = {
id: string
name: string
mode: string
description?: string
tags?: string[]
prompt_content?: string
model_settings?: {
model_name: string
parameters: Record<string, any>
}
created_at: string
updated_at: string
}
export type PromptTemplateRequest = {
name: string
mode: string
prompt_content: string
description?: string
tags?: string[]
model_name?: string
model_parameters?: Record<string, any>
}

@ -0,0 +1,22 @@
import { get, post, del, put } from './base'
import type { PromptTemplate, PromptTemplateRequest } from '@/models/prompt-template'
export const fetchPromptTemplates = async () => {
return get<{ data: PromptTemplate[] }>('/prompt-templates')
}
export const createPromptTemplate = async (data: PromptTemplateRequest) => {
return post('/prompt-templates', { body: data })
}
export const deletePromptTemplate = async (id: string) => {
return del(`/prompt-templates/${id}`)
}
export const getPromptTemplate = async (id: string) => {
return get<PromptTemplate>(`/prompt-templates/${id}`)
}
export const updatePromptTemplate = async (id: string, data: Partial<PromptTemplate>) => {
return put(`/prompt-templates/${id}`, { body: data })
}
Loading…
Cancel
Save