diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 13fd579e20..3f57a346cd 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -1,5 +1,6 @@ import contextlib import json +import logging from collections.abc import Generator, Iterable from copy import deepcopy from datetime import UTC, datetime @@ -36,6 +37,8 @@ from extensions.ext_database import db from models.enums import CreatorUserRole from models.model import Message, MessageFile +logger = logging.getLogger(__name__) + class ToolEngine: """ @@ -123,25 +126,31 @@ class ToolEngine: # transform tool invoke message to get LLM friendly message return plain_text, message_files, meta except ToolProviderCredentialValidationError as e: + logger.error(e, exc_info=True) error_response = "Please check your tool provider credentials" agent_tool_callback.on_tool_error(e) except (ToolNotFoundError, ToolNotSupportedError, ToolProviderNotFoundError) as e: error_response = f"there is not a tool named {tool.entity.identity.name}" + logger.error(e, exc_info=True) agent_tool_callback.on_tool_error(e) except ToolParameterValidationError as e: error_response = f"tool parameters validation error: {e}, please check your tool parameters" agent_tool_callback.on_tool_error(e) + logger.error(e, exc_info=True) except ToolInvokeError as e: error_response = f"tool invoke error: {e}" agent_tool_callback.on_tool_error(e) + logger.error(e, exc_info=True) except ToolEngineInvokeError as e: meta = e.meta error_response = f"tool invoke error: {meta.error}" agent_tool_callback.on_tool_error(e) + logger.error(e, exc_info=True) return error_response, [], meta except Exception as e: error_response = f"unknown error: {e}" agent_tool_callback.on_tool_error(e) + logger.error(e, exc_info=True) return error_response, [], ToolInvokeMeta.error_instance(error_response) diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 283744b43b..9c1ceff145 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -20,7 +20,6 @@ from core.tools.entities.tool_entities import ( ) from core.tools.errors import ToolInvokeError from factories.file_factory import build_from_mapping -from libs.login import current_user from models import Account, Tenant from models.model import App, EndUser from models.workflow import Workflow @@ -28,21 +27,6 @@ from models.workflow import Workflow logger = logging.getLogger(__name__) -def _try_resolve_user_from_request() -> Account | EndUser | None: - """ - Try to resolve user from Flask request context. - - Returns None if not in a request context or if user is not available. - """ - # Note: `current_user` is a LocalProxy. Never compare it with None directly. - # Use _get_current_object() to dereference the proxy - user = getattr(current_user, "_get_current_object", lambda: current_user)() - # Check if we got a valid user object - if user is not None and hasattr(user, "id"): - return user - return None - - class WorkflowTool(Tool): """ Workflow tool. @@ -223,12 +207,6 @@ class WorkflowTool(Tool): Returns: Account | EndUser | None: The resolved user object, or None if resolution fails. """ - # Try to resolve user from request context first - user = _try_resolve_user_from_request() - if user is not None: - return user - - # Fall back to database resolution return self._resolve_user_from_database(user_id=user_id) def _resolve_user_from_database(self, user_id: str) -> Account | EndUser | None: diff --git a/api/migrations/versions/2025_11_06_1603-9e6fa5cbcd80_make_message_annotation_question_not_.py b/api/migrations/versions/2025_11_06_1603-9e6fa5cbcd80_make_message_annotation_question_not_.py new file mode 100644 index 0000000000..624be1d073 --- /dev/null +++ b/api/migrations/versions/2025_11_06_1603-9e6fa5cbcd80_make_message_annotation_question_not_.py @@ -0,0 +1,60 @@ +"""make message annotation question not nullable + +Revision ID: 9e6fa5cbcd80 +Revises: 03f8dcbc611e +Create Date: 2025-11-06 16:03:54.549378 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9e6fa5cbcd80' +down_revision = '288345cd01d1' +branch_labels = None +depends_on = None + + +def upgrade(): + bind = op.get_bind() + message_annotations = sa.table( + "message_annotations", + sa.column("id", sa.String), + sa.column("message_id", sa.String), + sa.column("question", sa.Text), + ) + messages = sa.table( + "messages", + sa.column("id", sa.String), + sa.column("query", sa.Text), + ) + update_question_from_message = ( + sa.update(message_annotations) + .where( + sa.and_( + message_annotations.c.question.is_(None), + message_annotations.c.message_id.isnot(None), + ) + ) + .values( + question=sa.select(sa.func.coalesce(messages.c.query, "")) + .where(messages.c.id == message_annotations.c.message_id) + .scalar_subquery() + ) + ) + bind.execute(update_question_from_message) + + fill_remaining_questions = ( + sa.update(message_annotations) + .where(message_annotations.c.question.is_(None)) + .values(question="") + ) + bind.execute(fill_remaining_questions) + with op.batch_alter_table('message_annotations', schema=None) as batch_op: + batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=False) + + +def downgrade(): + with op.batch_alter_table('message_annotations', schema=None) as batch_op: + batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=True) diff --git a/api/models/model.py b/api/models/model.py index d6a0aa3bb3..72f2d173cc 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1423,7 +1423,7 @@ class MessageAnnotation(Base): app_id: Mapped[str] = mapped_column(StringUUID) conversation_id: Mapped[str | None] = mapped_column(StringUUID, sa.ForeignKey("conversations.id")) message_id: Mapped[str | None] = mapped_column(StringUUID) - question: Mapped[str | None] = mapped_column(LongText, nullable=True) + question: Mapped[str] = mapped_column(LongText, nullable=False) content: Mapped[str] = mapped_column(LongText, nullable=False) hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) account_id: Mapped[str] = mapped_column(StringUUID, nullable=False) diff --git a/api/services/annotation_service.py b/api/services/annotation_service.py index b73302508a..56e9cc6a00 100644 --- a/api/services/annotation_service.py +++ b/api/services/annotation_service.py @@ -209,8 +209,12 @@ class AppAnnotationService: if not app: raise NotFound("App not found") + question = args.get("question") + if question is None: + raise ValueError("'question' is required") + annotation = MessageAnnotation( - app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id + app_id=app.id, content=args["answer"], question=question, account_id=current_user.id ) db.session.add(annotation) db.session.commit() @@ -219,7 +223,7 @@ class AppAnnotationService: if annotation_setting: add_annotation_to_index_task.delay( annotation.id, - args["question"], + question, current_tenant_id, app_id, annotation_setting.collection_binding_id, @@ -244,8 +248,12 @@ class AppAnnotationService: if not annotation: raise NotFound("Annotation not found") + question = args.get("question") + if question is None: + raise ValueError("'question' is required") + annotation.content = args["answer"] - annotation.question = args["question"] + annotation.question = question db.session.commit() # if annotation reply is enabled , add annotation to index diff --git a/api/tests/test_containers_integration_tests/services/test_annotation_service.py b/api/tests/test_containers_integration_tests/services/test_annotation_service.py index 5555400ca6..4f5190e533 100644 --- a/api/tests/test_containers_integration_tests/services/test_annotation_service.py +++ b/api/tests/test_containers_integration_tests/services/test_annotation_service.py @@ -220,6 +220,23 @@ class TestAnnotationService: # Note: In this test, no annotation setting exists, so task should not be called mock_external_service_dependencies["add_task"].delay.assert_not_called() + def test_insert_app_annotation_directly_requires_question( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Question must be provided when inserting annotations directly. + """ + fake = Faker() + app, _ = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) + + annotation_args = { + "question": None, + "answer": fake.text(max_nb_chars=200), + } + + with pytest.raises(ValueError): + AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id) + def test_insert_app_annotation_directly_app_not_found( self, db_session_with_containers, mock_external_service_dependencies ): diff --git a/web/README.md b/web/README.md index 13780eec6c..9c731a081a 100644 --- a/web/README.md +++ b/web/README.md @@ -138,7 +138,7 @@ This will help you determine the testing strategy. See [web/testing/testing.md]( ## Documentation -Visit to view the full documentation. +Visit to view the full documentation. ## Community diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 81b4f2474e..f07b2932c9 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -5,7 +5,6 @@ import type { BlockEnum } from '@/app/components/workflow/types' import type { UpdateAppSiteCodeResponse } from '@/models/app' import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' -import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -17,7 +16,6 @@ import { ToastContext } from '@/app/components/base/toast' import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' import { isTriggerNode } from '@/app/components/workflow/types' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { useDocLink } from '@/context/i18n' import { fetchAppDetail, updateAppSiteAccessToken, @@ -36,7 +34,6 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() - const docLink = useDocLink() const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) @@ -59,25 +56,13 @@ const CardView: FC = ({ appId, isInPanel, className }) => { const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false const disableAppCards = !shouldRenderAppCards - const triggerDocUrl = docLink('/guides/workflow/node/start') const buildTriggerModeMessage = useCallback((featureName: string) => (
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
- { - event.stopPropagation() - }} - > - {t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })} -
- ), [t, triggerDocUrl]) + ), [t]) const disableWebAppTooltip = disableAppCards ? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' })) diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx index 60627e12c2..827986f521 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx @@ -1,12 +1,6 @@ import { render, screen } from '@testing-library/react' -import * as React from 'react' import HistoryPanel from './history-panel' -const mockDocLink = vi.fn(() => 'doc-link') -vi.mock('@/context/i18n', () => ({ - useDocLink: () => mockDocLink, -})) - vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({ default: ({ onClick }: { onClick: () => void }) => (