mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat-agent-mask
This commit is contained in:
commit
d9314bf748
|
|
@ -15,11 +15,12 @@ from controllers.console.wraps import (
|
|||
setup_required,
|
||||
)
|
||||
from core.ops.ops_trace_manager import OpsTraceManager
|
||||
from core.workflow.enums import NodeType
|
||||
from extensions.ext_database import db
|
||||
from fields.app_fields import app_detail_fields, app_detail_fields_with_site, app_pagination_fields
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from libs.validators import validate_description_length
|
||||
from models import App
|
||||
from models import App, Workflow
|
||||
from services.app_dsl_service import AppDslService, ImportMode
|
||||
from services.app_service import AppService
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
|
|
@ -106,6 +107,35 @@ class AppListApi(Resource):
|
|||
if str(app.id) in res:
|
||||
app.access_mode = res[str(app.id)].access_mode
|
||||
|
||||
workflow_capable_app_ids = [
|
||||
str(app.id) for app in app_pagination.items if app.mode in {"workflow", "advanced-chat"}
|
||||
]
|
||||
draft_trigger_app_ids: set[str] = set()
|
||||
if workflow_capable_app_ids:
|
||||
draft_workflows = (
|
||||
db.session.execute(
|
||||
select(Workflow).where(
|
||||
Workflow.version == Workflow.VERSION_DRAFT,
|
||||
Workflow.app_id.in_(workflow_capable_app_ids),
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
trigger_node_types = {
|
||||
NodeType.TRIGGER_WEBHOOK,
|
||||
NodeType.TRIGGER_SCHEDULE,
|
||||
NodeType.TRIGGER_PLUGIN,
|
||||
}
|
||||
for workflow in draft_workflows:
|
||||
for _, node_data in workflow.walk_nodes():
|
||||
if node_data.get("type") in trigger_node_types:
|
||||
draft_trigger_app_ids.add(str(workflow.app_id))
|
||||
break
|
||||
|
||||
for app in app_pagination.items:
|
||||
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
|
||||
|
||||
return marshal(app_pagination, app_pagination_fields), 200
|
||||
|
||||
@api.doc("create_app")
|
||||
|
|
|
|||
|
|
@ -152,13 +152,15 @@ class WordExtractor(BaseExtractor):
|
|||
# Initialize a row, all of which are empty by default
|
||||
row_cells = [""] * total_cols
|
||||
col_index = 0
|
||||
for cell in row.cells:
|
||||
while col_index < len(row.cells):
|
||||
# make sure the col_index is not out of range
|
||||
while col_index < total_cols and row_cells[col_index] != "":
|
||||
while col_index < len(row.cells) and row_cells[col_index] != "":
|
||||
col_index += 1
|
||||
# if col_index is out of range the loop is jumped
|
||||
if col_index >= total_cols:
|
||||
if col_index >= len(row.cells):
|
||||
break
|
||||
# get the correct cell
|
||||
cell = row.cells[col_index]
|
||||
cell_content = self._parse_cell(cell, image_map).strip()
|
||||
cell_colspan = cell.grid_span or 1
|
||||
for i in range(cell_colspan):
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ app_partial_fields = {
|
|||
"access_mode": fields.String,
|
||||
"create_user_name": fields.String,
|
||||
"author_name": fields.String,
|
||||
"has_draft_trigger": fields.Boolean,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
"""Primarily used for testing merged cell scenarios"""
|
||||
|
||||
from docx import Document
|
||||
|
||||
from core.rag.extractor.word_extractor import WordExtractor
|
||||
|
||||
|
||||
def _generate_table_with_merged_cells():
|
||||
doc = Document()
|
||||
|
||||
"""
|
||||
The table looks like this:
|
||||
+-----+-----+-----+
|
||||
| 1-1 & 1-2 | 1-3 |
|
||||
+-----+-----+-----+
|
||||
| 2-1 | 2-2 | 2-3 |
|
||||
| & |-----+-----+
|
||||
| 3-1 | 3-2 | 3-3 |
|
||||
+-----+-----+-----+
|
||||
"""
|
||||
table = doc.add_table(rows=3, cols=3)
|
||||
table.style = "Table Grid"
|
||||
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
cell = table.cell(i, j)
|
||||
cell.text = f"{i + 1}-{j + 1}"
|
||||
|
||||
# Merge cells
|
||||
cell_0_0 = table.cell(0, 0)
|
||||
cell_0_1 = table.cell(0, 1)
|
||||
merged_cell_1 = cell_0_0.merge(cell_0_1)
|
||||
merged_cell_1.text = "1-1 & 1-2"
|
||||
|
||||
cell_1_0 = table.cell(1, 0)
|
||||
cell_2_0 = table.cell(2, 0)
|
||||
merged_cell_2 = cell_1_0.merge(cell_2_0)
|
||||
merged_cell_2.text = "2-1 & 3-1"
|
||||
|
||||
ground_truth = [["1-1 & 1-2", "", "1-3"], ["2-1 & 3-1", "2-2", "2-3"], ["2-1 & 3-1", "3-2", "3-3"]]
|
||||
|
||||
return doc.tables[0], ground_truth
|
||||
|
||||
|
||||
def test_parse_row():
|
||||
table, gt = _generate_table_with_merged_cells()
|
||||
extractor = object.__new__(WordExtractor)
|
||||
for idx, row in enumerate(table.rows):
|
||||
assert extractor._parse_row(row, {}, 3) == gt[idx]
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
Utilities for detecting if database service is available for workflow tests.
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
import pytest
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
|
||||
def is_database_available() -> bool:
|
||||
"""
|
||||
Check if the database service is available by attempting to connect to it.
|
||||
|
||||
Returns:
|
||||
True if database is available, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Try to establish a database connection using a context manager
|
||||
with psycopg2.connect(
|
||||
host=dify_config.DB_HOST,
|
||||
port=dify_config.DB_PORT,
|
||||
database=dify_config.DB_DATABASE,
|
||||
user=dify_config.DB_USERNAME,
|
||||
password=dify_config.DB_PASSWORD,
|
||||
connect_timeout=2, # 2 second timeout
|
||||
) as conn:
|
||||
pass # Connection established and will be closed automatically
|
||||
return True
|
||||
except (psycopg2.OperationalError, psycopg2.Error):
|
||||
return False
|
||||
|
||||
|
||||
def skip_if_database_unavailable():
|
||||
"""
|
||||
Pytest skip decorator that skips tests when database service is unavailable.
|
||||
|
||||
Usage:
|
||||
@skip_if_database_unavailable()
|
||||
def test_my_workflow():
|
||||
...
|
||||
"""
|
||||
return pytest.mark.skipif(
|
||||
not is_database_available(),
|
||||
reason="Database service is not available (connection refused or authentication failed)",
|
||||
)
|
||||
|
|
@ -6,9 +6,11 @@ This module tests the iteration node's ability to:
|
|||
2. Preserve nested array structure when flatten_output=False
|
||||
"""
|
||||
|
||||
from .test_database_utils import skip_if_database_unavailable
|
||||
from .test_table_runner import TableTestRunner, WorkflowTestCase
|
||||
|
||||
|
||||
@skip_if_database_unavailable()
|
||||
def test_iteration_with_flatten_output_enabled():
|
||||
"""
|
||||
Test iteration node with flatten_output=True (default behavior).
|
||||
|
|
@ -37,6 +39,7 @@ def test_iteration_with_flatten_output_enabled():
|
|||
)
|
||||
|
||||
|
||||
@skip_if_database_unavailable()
|
||||
def test_iteration_with_flatten_output_disabled():
|
||||
"""
|
||||
Test iteration node with flatten_output=False.
|
||||
|
|
@ -65,6 +68,7 @@ def test_iteration_with_flatten_output_disabled():
|
|||
)
|
||||
|
||||
|
||||
@skip_if_database_unavailable()
|
||||
def test_iteration_flatten_output_comparison():
|
||||
"""
|
||||
Run both flatten_output configurations in parallel to verify the difference.
|
||||
|
|
|
|||
|
|
@ -282,21 +282,23 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
</>
|
||||
)}
|
||||
{
|
||||
(!systemFeatures.webapp_auth.enabled)
|
||||
? <>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
|
||||
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
|
||||
</button>
|
||||
</>
|
||||
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
|
||||
<>
|
||||
!app.has_draft_trigger && (
|
||||
(!systemFeatures.webapp_auth.enabled)
|
||||
? <>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
|
||||
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
|
||||
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
<Divider className="my-1" />
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ const RegenerationModal: FC<IRegenerationModalProps> = ({
|
|||
})
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={noop} className='!max-w-[480px] !rounded-2xl'>
|
||||
<Modal isShow={isShow} onClose={noop} className='!max-w-[480px] !rounded-2xl' wrapperClassName='!z-[10000]'>
|
||||
{!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
|
||||
{loading && !updateSucceeded && <RegeneratingContent />}
|
||||
{!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||
const [limit, setLimit] = useState(DEFAULT_LIMIT)
|
||||
const [fullScreen, setFullScreen] = useState(false)
|
||||
const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false)
|
||||
const [isRegenerationModalOpen, setIsRegenerationModalOpen] = useState(false)
|
||||
|
||||
const segmentListRef = useRef<HTMLDivElement>(null)
|
||||
const childSegmentListRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -669,6 +670,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||
onClose={onCloseSegmentDetail}
|
||||
showOverlay={false}
|
||||
needCheckChunks
|
||||
modal={isRegenerationModalOpen}
|
||||
>
|
||||
<SegmentDetail
|
||||
key={currSegment.segInfo?.id}
|
||||
|
|
@ -677,6 +679,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||
isEditMode={currSegment.isEditMode}
|
||||
onUpdate={handleUpdateSegment}
|
||||
onCancel={onCloseSegmentDetail}
|
||||
onModalStateChange={setIsRegenerationModalOpen}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Create New Segment */}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type ISegmentDetailProps = {
|
|||
onCancel: () => void
|
||||
isEditMode?: boolean
|
||||
docForm: ChunkingMode
|
||||
onModalStateChange?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,6 +39,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||
onCancel,
|
||||
isEditMode,
|
||||
docForm,
|
||||
onModalStateChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [question, setQuestion] = useState(isEditMode ? segInfo?.content || '' : segInfo?.sign_content || '')
|
||||
|
|
@ -68,11 +70,19 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||
|
||||
const handleRegeneration = useCallback(() => {
|
||||
setShowRegenerationModal(true)
|
||||
}, [])
|
||||
onModalStateChange?.(true)
|
||||
}, [onModalStateChange])
|
||||
|
||||
const onCancelRegeneration = useCallback(() => {
|
||||
setShowRegenerationModal(false)
|
||||
}, [])
|
||||
onModalStateChange?.(false)
|
||||
}, [onModalStateChange])
|
||||
|
||||
const onCloseAfterRegeneration = useCallback(() => {
|
||||
setShowRegenerationModal(false)
|
||||
onModalStateChange?.(false)
|
||||
onCancel() // Close the edit drawer
|
||||
}, [onCancel, onModalStateChange])
|
||||
|
||||
const onConfirmRegeneration = useCallback(() => {
|
||||
onUpdate(segInfo?.id || '', question, answer, keywords, true)
|
||||
|
|
@ -161,7 +171,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||
isShow={showRegenerationModal}
|
||||
onConfirm={onConfirmRegeneration}
|
||||
onCancel={onCancelRegeneration}
|
||||
onClose={onCancelRegeneration}
|
||||
onClose={onCloseAfterRegeneration}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -379,6 +379,8 @@ export type App = {
|
|||
/** access control */
|
||||
access_mode: AccessMode
|
||||
max_active_requests?: number | null
|
||||
/** whether workflow trigger has un-published draft */
|
||||
has_draft_trigger?: boolean
|
||||
}
|
||||
|
||||
export type AppSSO = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue