Merge branch 'main' into deploy/dev
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions

This commit is contained in:
Stephen Zhou 2026-01-11 21:54:32 +08:00
commit 049925c5d8
No known key found for this signature in database
13 changed files with 585 additions and 940 deletions

View File

@ -1,6 +1,7 @@
import json
import logging
import uuid
from decimal import Decimal
from typing import Union, cast
from sqlalchemy import select
@ -41,6 +42,7 @@ from core.tools.tool_manager import ToolManager
from core.tools.utils.dataset_retriever_tool import DatasetRetrieverTool
from extensions.ext_database import db
from factories import file_factory
from models.enums import CreatorUserRole
from models.model import Conversation, Message, MessageAgentThought, MessageFile
logger = logging.getLogger(__name__)
@ -300,6 +302,7 @@ class BaseAgentRunner(AppRunner):
thought = MessageAgentThought(
message_id=message_id,
message_chain_id=None,
tool_process_data=None,
thought="",
tool=tool_name,
tool_labels_str="{}",
@ -307,20 +310,20 @@ class BaseAgentRunner(AppRunner):
tool_input=tool_input,
message=message,
message_token=0,
message_unit_price=0,
message_price_unit=0,
message_unit_price=Decimal(0),
message_price_unit=Decimal("0.001"),
message_files=json.dumps(messages_ids) if messages_ids else "",
answer="",
observation="",
answer_token=0,
answer_unit_price=0,
answer_price_unit=0,
answer_unit_price=Decimal(0),
answer_price_unit=Decimal("0.001"),
tokens=0,
total_price=0,
total_price=Decimal(0),
position=self.agent_thought_count + 1,
currency="USD",
latency=0,
created_by_role="account",
created_by_role=CreatorUserRole.ACCOUNT,
created_by=self.user_id,
)
@ -353,7 +356,8 @@ class BaseAgentRunner(AppRunner):
raise ValueError("agent thought not found")
if thought:
agent_thought.thought += thought
existing_thought = agent_thought.thought or ""
agent_thought.thought = f"{existing_thought}{thought}"
if tool_name:
agent_thought.tool = tool_name
@ -451,21 +455,30 @@ class BaseAgentRunner(AppRunner):
agent_thoughts: list[MessageAgentThought] = message.agent_thoughts
if agent_thoughts:
for agent_thought in agent_thoughts:
tools = agent_thought.tool
if tools:
tools = tools.split(";")
tool_names_raw = agent_thought.tool
if tool_names_raw:
tool_names = tool_names_raw.split(";")
tool_calls: list[AssistantPromptMessage.ToolCall] = []
tool_call_response: list[ToolPromptMessage] = []
try:
tool_inputs = json.loads(agent_thought.tool_input)
except Exception:
tool_inputs = {tool: {} for tool in tools}
try:
tool_responses = json.loads(agent_thought.observation)
except Exception:
tool_responses = dict.fromkeys(tools, agent_thought.observation)
tool_input_payload = agent_thought.tool_input
if tool_input_payload:
try:
tool_inputs = json.loads(tool_input_payload)
except Exception:
tool_inputs = {tool: {} for tool in tool_names}
else:
tool_inputs = {tool: {} for tool in tool_names}
for tool in tools:
observation_payload = agent_thought.observation
if observation_payload:
try:
tool_responses = json.loads(observation_payload)
except Exception:
tool_responses = dict.fromkeys(tool_names, observation_payload)
else:
tool_responses = dict.fromkeys(tool_names, observation_payload)
for tool in tool_names:
# generate a uuid for tool call
tool_call_id = str(uuid.uuid4())
tool_calls.append(
@ -495,7 +508,7 @@ class BaseAgentRunner(AppRunner):
*tool_call_response,
]
)
if not tools:
if not tool_names_raw:
result.append(AssistantPromptMessage(content=agent_thought.thought))
else:
if message.answer:

View File

@ -1915,7 +1915,7 @@ class MessageChain(TypeBase):
)
class MessageAgentThought(Base):
class MessageAgentThought(TypeBase):
__tablename__ = "message_agent_thoughts"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="message_agent_thought_pkey"),
@ -1923,34 +1923,42 @@ class MessageAgentThought(Base):
sa.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"),
)
id = mapped_column(StringUUID, default=lambda: str(uuid4()))
message_id = mapped_column(StringUUID, nullable=False)
message_chain_id = mapped_column(StringUUID, nullable=True)
id: Mapped[str] = mapped_column(
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
)
message_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
position: Mapped[int] = mapped_column(sa.Integer, nullable=False)
thought = mapped_column(LongText, nullable=True)
tool = mapped_column(LongText, nullable=True)
tool_labels_str = mapped_column(LongText, nullable=False, default=sa.text("'{}'"))
tool_meta_str = mapped_column(LongText, nullable=False, default=sa.text("'{}'"))
tool_input = mapped_column(LongText, nullable=True)
observation = mapped_column(LongText, nullable=True)
created_by_role: Mapped[str] = mapped_column(String(255), nullable=False)
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
message_chain_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
thought: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
tool: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
tool_labels_str: Mapped[str] = mapped_column(LongText, nullable=False, default=sa.text("'{}'"))
tool_meta_str: Mapped[str] = mapped_column(LongText, nullable=False, default=sa.text("'{}'"))
tool_input: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
observation: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
# plugin_id = mapped_column(StringUUID, nullable=True) ## for future design
tool_process_data = mapped_column(LongText, nullable=True)
message = mapped_column(LongText, nullable=True)
message_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
message_unit_price = mapped_column(sa.Numeric, nullable=True)
message_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
message_files = mapped_column(LongText, nullable=True)
answer = mapped_column(LongText, nullable=True)
answer_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
answer_unit_price = mapped_column(sa.Numeric, nullable=True)
answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
total_price = mapped_column(sa.Numeric, nullable=True)
currency = mapped_column(String(255), nullable=True)
latency: Mapped[float | None] = mapped_column(sa.Float, nullable=True)
created_by_role = mapped_column(String(255), nullable=False)
created_by = mapped_column(StringUUID, nullable=False)
created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.func.current_timestamp())
tool_process_data: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
message: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
message_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True, default=None)
message_unit_price: Mapped[Decimal | None] = mapped_column(sa.Numeric, nullable=True, default=None)
message_price_unit: Mapped[Decimal] = mapped_column(
sa.Numeric(10, 7), nullable=False, default=Decimal("0.001"), server_default=sa.text("0.001")
)
message_files: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
answer: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
answer_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True, default=None)
answer_unit_price: Mapped[Decimal | None] = mapped_column(sa.Numeric, nullable=True, default=None)
answer_price_unit: Mapped[Decimal] = mapped_column(
sa.Numeric(10, 7), nullable=False, default=Decimal("0.001"), server_default=sa.text("0.001")
)
tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True, default=None)
total_price: Mapped[Decimal | None] = mapped_column(sa.Numeric, nullable=True, default=None)
currency: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
latency: Mapped[float | None] = mapped_column(sa.Float, nullable=True, default=None)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime, nullable=False, init=False, server_default=sa.func.current_timestamp()
)
@property
def files(self) -> list[Any]:

View File

@ -230,7 +230,6 @@ class TestAgentService:
# Create first agent thought
thought1 = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to analyze the user's request",
@ -257,7 +256,6 @@ class TestAgentService:
# Create second agent thought
thought2 = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=2,
thought="Based on the analysis, I can provide a response",
@ -545,7 +543,6 @@ class TestAgentService:
# Create agent thought with tool error
thought_with_error = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to analyze the user's request",
@ -759,7 +756,6 @@ class TestAgentService:
# Create agent thought with multiple tools
complex_thought = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to use multiple tools to complete this task",
@ -877,7 +873,6 @@ class TestAgentService:
# Create agent thought with files
thought_with_files = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to process some files",
@ -957,7 +952,6 @@ class TestAgentService:
# Create agent thought with empty tool data
empty_thought = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to analyze the user's request",
@ -999,7 +993,6 @@ class TestAgentService:
# Create agent thought with malformed JSON
malformed_thought = MessageAgentThought(
id=fake.uuid4(),
message_id=message.id,
position=1,
thought="I need to analyze the user's request",

View File

@ -1,8 +1,10 @@
import type { Preview } from '@storybook/react'
import type { Resource } from 'i18next'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ToastProvider } from '../app/components/base/toast'
import I18N from '../app/components/i18n'
import { I18nClientProvider as I18N } from '../app/components/provider/i18n'
import commonEnUS from '../i18n/en-US/common.json'
import '../app/styles/globals.css'
import '../app/styles/markdown.scss'
@ -16,6 +18,14 @@ const queryClient = new QueryClient({
},
})
const storyResources: Resource = {
'en-US': {
// Preload the most common namespace to avoid missing keys during initial render;
// other namespaces will be loaded on demand via resourcesToBackend.
common: commonEnUS as unknown as Record<string, unknown>,
},
}
export const decorators = [
withThemeByDataAttribute({
themes: {
@ -28,7 +38,7 @@ export const decorators = [
(Story) => {
return (
<QueryClientProvider client={queryClient}>
<I18N locale="en-US">
<I18N locale="en-US" resource={storyResources}>
<ToastProvider>
<Story />
</ToastProvider>

View File

@ -0,0 +1,3 @@
'use client'
export { SerwistProvider } from '@serwist/turbopack/react'

View File

@ -12,6 +12,7 @@ import { ToastProvider } from './components/base/toast'
import BrowserInitializer from './components/browser-initializer'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
import { I18nServerProvider } from './components/provider/i18n-server'
import { SerwistProvider } from './components/provider/serwist'
import SentryInitializer from './components/sentry-initializer'
import RoutePrefixHandle from './routePrefixHandle'
import './styles/globals.css'
@ -39,6 +40,9 @@ const LocaleLayout = async ({
}) => {
const locale = await getLocaleOnServer()
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const swUrl = `${basePath}/serwist/sw.js`
const datasetMap: Record<DatasetAttr, string | undefined> = {
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
[DatasetAttr.DATA_PUBLIC_API_PREFIX]: process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX,
@ -92,33 +96,35 @@ const LocaleLayout = async ({
className="color-scheme h-full select-auto"
{...datasetMap}
>
<ReactScanLoader />
<JotaiProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
<SerwistProvider swUrl={swUrl}>
<ReactScanLoader />
<JotaiProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
</SerwistProvider>
</body>
</html>
)

View File

@ -0,0 +1,14 @@
import { spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { createSerwistRoute } from '@serwist/turbopack'
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const revision = spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' }).stdout?.trim() || randomUUID()
export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute({
additionalPrecacheEntries: [{ url: `${basePath}/_offline.html`, revision }],
swSrc: 'app/sw.ts',
nextConfig: {
basePath,
},
})

104
web/app/sw.ts Normal file
View File

@ -0,0 +1,104 @@
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'
import { CacheableResponsePlugin, CacheFirst, ExpirationPlugin, NetworkFirst, Serwist, StaleWhileRevalidate } from 'serwist'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined
}
}
declare const self: ServiceWorkerGlobalScope
const scopePathname = new URL(self.registration.scope).pathname
const basePath = scopePathname.replace(/\/serwist\/$/, '').replace(/\/$/, '')
const offlineUrl = `${basePath}/_offline.html`
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: [
{
matcher: ({ url }) => url.origin === 'https://fonts.googleapis.com',
handler: new CacheFirst({
cacheName: 'google-fonts',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ url }) => url.origin === 'https://fonts.gstatic.com',
handler: new CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ request }) => request.destination === 'image',
handler: new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 64,
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ request }) => request.destination === 'script' || request.destination === 'style',
handler: new StaleWhileRevalidate({
cacheName: 'static-resources',
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/api/'),
handler: new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
plugins: [
new ExpirationPlugin({
maxEntries: 16,
maxAgeSeconds: 60 * 60,
}),
],
}),
},
],
fallbacks: {
entries: [
{
url: offlineUrl,
matcher({ request }) {
return request.destination === 'document'
},
},
],
},
})
serwist.addEventListeners()

View File

@ -15,10 +15,7 @@ const config: KnipConfig = {
ignoreBinaries: [
'only-allow',
],
ignoreDependencies: [
// required by next-pwa
'babel-loader',
],
ignoreDependencies: [],
rules: {
files: 'warn',
dependencies: 'warn',

View File

@ -1,77 +1,8 @@
import withBundleAnalyzerInit from '@next/bundle-analyzer'
import createMDX from '@next/mdx'
import { codeInspectorPlugin } from 'code-inspector-plugin'
import withPWAInit from 'next-pwa'
const isDev = process.env.NODE_ENV === 'development'
const withPWA = withPWAInit({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
fallbacks: {
document: '/_offline.html',
},
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-webfonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 64,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
{
urlPattern: /\.(?:js|css)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-resources',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 1 day
},
},
},
{
urlPattern: /^\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 16,
maxAgeSeconds: 60 * 60, // 1 hour
},
},
},
],
})
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
@ -97,6 +28,7 @@ const remoteImageURLs = [hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WE
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
serverExternalPackages: ['esbuild-wasm'],
transpilePackages: ['echarts', 'zrender'],
turbopack: {
rules: codeInspectorPlugin({
@ -148,4 +80,4 @@ const nextConfig = {
},
}
export default withPWA(withBundleAnalyzer(withMDX(nextConfig)))
export default withBundleAnalyzer(withMDX(nextConfig))

View File

@ -111,7 +111,6 @@
"mitt": "^3.0.1",
"negotiator": "^1.0.0",
"next": "~15.5.9",
"next-pwa": "^5.6.0",
"next-themes": "^0.4.6",
"nuqs": "^2.8.6",
"pinyin-pro": "^3.27.0",
@ -153,7 +152,6 @@
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.3",
"@babel/core": "^7.28.4",
"@chromatic-com/storybook": "^4.1.1",
"@eslint-react/eslint-plugin": "^2.3.13",
"@mdx-js/loader": "^3.1.1",
@ -162,6 +160,7 @@
"@next/eslint-plugin-next": "15.5.9",
"@next/mdx": "15.5.9",
"@rgrove/parse-xml": "^4.2.0",
"@serwist/turbopack": "^9.5.0",
"@storybook/addon-docs": "9.1.13",
"@storybook/addon-links": "9.1.13",
"@storybook/addon-onboarding": "9.1.13",
@ -194,9 +193,9 @@
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "4.0.16",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"code-inspector-plugin": "1.2.9",
"cross-env": "^10.1.0",
"esbuild-wasm": "^0.27.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
@ -212,6 +211,7 @@
"postcss": "^8.5.6",
"react-scan": "^0.4.3",
"sass": "^1.93.2",
"serwist": "^9.5.0",
"storybook": "9.1.17",
"tailwindcss": "^3.4.18",
"tsx": "^4.21.0",

1124
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long