support mark inner api

pull/21891/head
ytqh 1 year ago
parent f36c378390
commit 71bc05235c

@ -0,0 +1,7 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint("inner_tools", __name__, url_prefix="/inner_tools")
api = ExternalApi(bp)
from . import markdown_to_pdf

@ -0,0 +1,151 @@
import io
import urllib.parse
import uuid
from pathlib import Path
from typing import Optional, Tuple
import markdown
from controllers.inner_tools import api
from core.tools.tool_file_manager import ToolFileManager
from flask import Response, jsonify, request
from flask_restful import Resource # type: ignore
from models.account import Tenant
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
class MarkdownToPDFApi(Resource):
def post(self):
"""Convert markdown to PDF
This endpoint takes markdown text and converts it to a PDF document.
Returns URL of uploaded PDF file.
No authentication required.
"""
# Parse request arguments
if not request.is_json:
return {"error": "Request must be JSON"}, 400
data = request.get_json()
markdown_text = data.get('markdown_text')
title = data.get('title', 'Document')
if not markdown_text:
return {"error": "markdown_text is required"}, 400
# get first tenant from database
tenant = Tenant.query.first()
if not tenant:
return {"error": "no tenant found"}, 400
tenant_id = tenant.id
# Generate PDF
pdf_binary, filename = self._generate_pdf_binary(markdown_text, title)
# Save the file using ToolFileManager
tool_file = ToolFileManager.create_file_by_raw(
user_id=None,
tenant_id=tenant_id,
conversation_id=None,
file_binary=pdf_binary,
mimetype='application/pdf',
)
# Return the file info with URL
file_url = ToolFileManager.sign_file(tool_file.id, '.pdf')
return jsonify({'url': file_url, 'file_id': tool_file.id, 'file_name': filename, 'file_size': tool_file.size})
def _generate_pdf_binary(self, markdown_text: str, title: str) -> Tuple[bytes, str]:
"""Generate PDF from markdown text and return the binary data and filename"""
# Convert markdown to HTML (for potential future enhancement)
html_content = markdown.markdown(markdown_text, extensions=['extra'])
# Create PDF using reportlab
buffer = io.BytesIO()
pdf = canvas.Canvas(buffer, pagesize=letter)
# Add title
pdf.setFont("Helvetica-Bold", 16)
pdf.drawString(72, 760, title)
# Add markdown content
pdf.setFont("Helvetica", 12)
y_position = 730
# Simple text rendering - each line as a separate line in the PDF
for line in markdown_text.split('\n'):
if line.startswith('# '): # H1 heading
pdf.setFont("Helvetica-Bold", 14)
pdf.drawString(72, y_position, line[2:])
y_position -= 20
pdf.setFont("Helvetica", 12)
elif line.startswith('## '): # H2 heading
pdf.setFont("Helvetica-Bold", 13)
pdf.drawString(72, y_position, line[3:])
y_position -= 18
pdf.setFont("Helvetica", 12)
elif line.startswith('### '): # H3 heading
pdf.setFont("Helvetica-Bold", 12)
pdf.drawString(72, y_position, line[4:])
y_position -= 16
pdf.setFont("Helvetica", 12)
elif line.strip() == '': # Empty line
y_position -= 12
else: # Regular text
pdf.drawString(72, y_position, line)
y_position -= 14
# Check if we need a new page
if y_position < 72:
pdf.showPage()
y_position = 760
pdf.setFont("Helvetica", 12)
# Save the PDF
pdf.save()
buffer.seek(0)
# Generate a filename based on the title
filename = f"{title.replace(' ', '_')}_{uuid.uuid4().hex[:8]}.pdf"
return buffer.getvalue(), filename
# Add API endpoint for getting PDF from stored file
class MarkdownToPDFFileApi(Resource):
def get(self, file_id):
"""Get a PDF file by its tool file ID
This endpoint retrieves a PDF file stored in the system using its tool file ID.
No authentication required.
"""
try:
# Get the file binary from ToolFileManager
result = ToolFileManager.get_file_binary(file_id)
if result is None:
return {"error": "File not found"}, 404
# Safely unpack result only after confirming it's not None
file_binary, mimetype = result
if mimetype != 'application/pdf':
return {"error": "File is not a PDF"}, 400
# Return the PDF
response = Response(
file_binary,
mimetype='application/pdf',
headers={
'Content-Disposition': f'attachment; filename="{file_id}.pdf"',
'Content-Type': 'application/pdf',
},
)
return response
except Exception as e:
return {"error": str(e)}, 500
api.add_resource(MarkdownToPDFApi, '/markdown-to-pdf')
api.add_resource(MarkdownToPDFFileApi, '/markdown-to-pdf/<string:file_id>')

@ -9,7 +9,6 @@ from typing import Optional, Union
from uuid import uuid4
import httpx
from configs import dify_config
from core.helper import ssrf_proxy
from extensions.ext_database import db
@ -58,7 +57,7 @@ class ToolFileManager:
@staticmethod
def create_file_by_raw(
*,
user_id: str,
user_id: Optional[str],
tenant_id: str,
conversation_id: Optional[str],
file_binary: bytes,

@ -5,13 +5,13 @@ from dify_app import DifyApp
def init_app(app: DifyApp):
# register blueprint routers
from flask_cors import CORS # type: ignore
from controllers.console import bp as console_app_bp
from controllers.files import bp as files_bp
from controllers.inner_api import bp as inner_api_bp
from controllers.inner_tools import bp as inner_tools_bp
from controllers.service_api import bp as service_api_bp
from controllers.web import bp as web_bp
from flask_cors import CORS # type: ignore
CORS(
service_api_bp,
@ -46,3 +46,5 @@ def init_app(app: DifyApp):
app.register_blueprint(files_bp)
app.register_blueprint(inner_api_bp)
app.register_blueprint(inner_tools_bp)

@ -0,0 +1,37 @@
"""make tool file user id optional
Revision ID: e927d80409a1
Revises: a91b476a53de
Create Date: 2025-04-05 21:00:53.060052
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e927d80409a1'
down_revision = 'a91b476a53de'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_files', schema=None) as batch_op:
batch_op.alter_column('user_id',
existing_type=sa.UUID(),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_files', schema=None) as batch_op:
batch_op.alter_column('user_id',
existing_type=sa.UUID(),
nullable=False)
# ### end Alembic commands ###

@ -2,12 +2,12 @@ import json
from typing import Any, Optional
import sqlalchemy as sa
from sqlalchemy import ForeignKey, func
from sqlalchemy.orm import Mapped, mapped_column
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_bundle import ApiToolBundle
from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
from core.tools.entities.tool_entities import (
ApiProviderSchemaType, WorkflowToolParameterConfiguration)
from sqlalchemy import ForeignKey, func
from sqlalchemy.orm import Mapped, mapped_column
from .engine import db
from .model import Account, App, Tenant
@ -294,7 +294,7 @@ class ToolFile(db.Model): # type: ignore[name-defined]
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
user_id: Mapped[str] = db.Column(StringUUID, nullable=False)
user_id: Mapped[Optional[str]] = db.Column(StringUUID, nullable=True)
tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False)
conversation_id: Mapped[Optional[str]] = db.Column(StringUUID, nullable=True)
file_key: Mapped[str] = db.Column(db.String(255), nullable=False)
@ -306,7 +306,7 @@ class ToolFile(db.Model): # type: ignore[name-defined]
def __init__(
self,
*,
user_id: str,
user_id: Optional[str],
tenant_id: str,
conversation_id: Optional[str] = None,
file_key: str,

653
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
[project]
name = "dify-api"
requires-python = ">=3.11,<3.13"
dynamic = [ "dependencies" ]
dynamic = ["dependencies"]
[build-system]
requires = ["poetry-core>=2.0.0"]
@ -89,9 +89,16 @@ tiktoken = "~0.8.0"
tokenizers = "~0.15.0"
transformers = "~4.35.0"
types-pytz = "~2024.2.0.20241003"
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] }
unstructured = { version = "~0.16.1", extras = [
"docx",
"epub",
"md",
"msg",
"ppt",
"pptx",
] }
validators = "0.21.0"
volcengine-python-sdk = {extras = ["ark"], version = "~1.0.98"}
volcengine-python-sdk = { extras = ["ark"], version = "~1.0.98" }
websocket-client = "~1.7.0"
xinference-client = "0.15.2"
yarl = "~1.18.3"
@ -124,8 +131,15 @@ nltk = "3.9.1"
numexpr = "~2.9.0"
pydub = "~0.25.1"
qrcode = "~7.4.2"
reportlab = "~3.6.12"
twilio = "~9.0.4"
vanna = { version = "0.7.5", extras = ["postgres", "mysql", "clickhouse", "duckdb", "oracle"] }
vanna = { version = "0.7.5", extras = [
"postgres",
"mysql",
"clickhouse",
"duckdb",
"oracle",
] }
wikipedia = "1.4.0"
yfinance = "~0.2.40"

Loading…
Cancel
Save