feat: Add prompt template feature
parent
37e19de7ab
commit
75f232d832
@ -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.
|
||||
|
||||
@ -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 ###
|
||||
@ -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
|
||||
@ -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…
Reference in New Issue