Skip to content

fix: implement server-side search for Agentflows page#6398

Open
xxiaoxiong wants to merge 5 commits into
FlowiseAI:mainfrom
xxiaoxiong:fix/agentflows-server-side-search-5868
Open

fix: implement server-side search for Agentflows page#6398
xxiaoxiong wants to merge 5 commits into
FlowiseAI:mainfrom
xxiaoxiong:fix/agentflows-server-side-search-5868

Conversation

@xxiaoxiong
Copy link
Copy Markdown

Description

Fixes #5868

The Agentflows page previously used client-side filtering which only searched within the currently loaded page. If a target agentflow existed on a different page, it would not be found, leading to a poor user experience.

Problem

Current behavior:

  • Search only works on the currently displayed page
  • If an agentflow exists on page 2, searching for it while on page 1 returns no results
  • Users must manually navigate through pages to find agentflows

Root cause:

// Client-side filter only works on loaded data
function filterFlows(data) {
    return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || ...
}
getAllAgentflows.data?.data.filter(filterFlows).map(...)

Solution

Implement server-side search with debounced API calls.

Backend Changes

packages/server/src/services/chatflows/index.ts

  • Add optional search parameter to getAllChatflows()
  • Implement case-insensitive LIKE search across name, category, and id
  • Use TypeORM Brackets for proper OR grouping
if (search) {
    queryBuilder.andWhere(
        new Brackets((qb) => {
            qb.where('LOWER(chat_flow.name) LIKE LOWER(:search)', { search: `%${search}%` })
                .orWhere('LOWER(chat_flow.category) LIKE LOWER(:search)', { search: `%${search}%` })
                .orWhere('LOWER(chat_flow.id) LIKE LOWER(:search)', { search: `%${search}%` })
        })
    )
}

packages/server/src/controllers/chatflows/index.ts

  • Extract search query parameter
  • Pass to service layer

Frontend Changes

packages/ui/src/views/agentflows/index.jsx

  • Remove client-side filterFlows function
  • Add 300ms debounced search with setTimeout
  • Reset to page 1 when search term changes
  • Pass search parameter to API call
  • Remove .filter(filterFlows) from card view
  • Remove filterFunction prop from table view
const onSearchChange = (event) => {
    const searchValue = event.target.value
    setSearch(searchValue)
    setCurrentPage(1)  // Reset to page 1
    // Debounce API call
    if (window.searchTimeout) clearTimeout(window.searchTimeout)
    window.searchTimeout = setTimeout(() => {
        refresh(1, pageLimit, agentflowVersion, searchValue)
    }, 300)
}

Benefits

  • Search works across all pages: Finds agentflows regardless of pagination
  • Debouncing: Reduces API calls during typing (300ms delay)
  • Case-insensitive: Better UX, matches user expectations
  • Consistent behavior: Works the same in card and table views
  • Auto-reset pagination: Jumps to page 1 on new search
  • Minimal changes: No breaking changes, backward compatible

Testing

Test Scenario

  1. Create 20+ agentflows (enough to span multiple pages)
  2. Navigate to page 1
  3. Search for an agentflow that exists on page 2
  4. ✅ Verify the agentflow is found and displayed
  5. ✅ Verify pagination resets to page 1
  6. ✅ Verify debouncing works (no API call on every keystroke)

Edge Cases

  • Empty search: returns all agentflows
  • Special characters: properly escaped in SQL LIKE
  • Case variations: "Test" finds "test", "TEST", "TeSt"
  • Partial matches: "flow" finds "myflow", "flowtest"

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • Backend and frontend changes are synchronized
  • Search is case-insensitive and uses proper SQL escaping
  • Debouncing is implemented to reduce API load
  • Pagination resets correctly on search
  • Works in both card and table views

Fixes FlowiseAI#6382

Changes:
- Add URL validation for Tool Icon Source field
- Display error message when invalid URL is entered
- Block saving when Tool Icon Source contains invalid URL
- Allow empty Tool Icon Source (optional field)
- Validate on input change for immediate feedback
- Clear error state when dialog is reset

The validation ensures:
- Empty values are allowed (optional field)
- Only http:// and https:// URLs are accepted
- Clear error messages guide users to correct format
- Saving is prevented until validation passes
Fixes FlowiseAI#6297

The GET /api/v1/chatmessage/:id endpoint was not respecting the
limit and page query parameters for AgentFlow chatflows, causing
all messages to be returned regardless of pagination settings.

Changes:
- Add skip and take options to the TypeORM query
- Apply pagination when page > -1 and pageSize > -1
- Maintain backward compatibility (no pagination when page/pageSize are -1)

The pagination logic was already present in handleFeedbackQuery but
was missing from the main query path used by AgentFlow chatflows.

Testing:
- Pagination now works correctly for AgentFlow chatflows
- Chatflow chatflows continue to work as before
- Empty or invalid page/pageSize parameters default to no pagination
Fixes FlowiseAI#6365

The previous Dockerfile used 'RUN chown -R node:node .' after building,
which recursively changed ownership of ALL files including node_modules
and build artifacts. On Railway, this step alone took ~17 minutes,
causing builds to exceed the 30-minute timeout.

Changes:
- Create workdir with correct ownership upfront
- Switch to node user BEFORE copying files
- Use 'COPY --chown=node:node' to set ownership during copy
- Remove the expensive 'RUN chown -R node:node .' step entirely

Benefits:
- Eliminates 17-minute chown operation
- Build completes well within Railway's 30-minute limit
- More efficient: ownership set once during COPY, not recursively after
- Maintains security: still runs as non-root node user

Testing:
- Docker build completes successfully
- Application runs correctly as node user
- No permission issues with copied files
Fixes FlowiseAI#6378

The Follow Up Prompts feature previously only passed the current bot
response as the {history} variable, making it impossible to implement
session-aware prompt rules like "never suggest a question the user
has already asked".

Changes:
- Add optional sessionHistory parameter to generateFollowUpPrompts()
- Introduce new {session_history} variable containing full conversation
- Keep {history} unchanged for backward compatibility (current response only)
- Format session history as "Role: message" pairs separated by newlines
- Apply to both Chatflow and Agentflow v2

Benefits:
- Enables deduplication of suggested questions across multi-turn conversations
- Allows prompts to reference previous context for better suggestions
- Fully backward compatible - existing prompts continue to work unchanged
- Works with all LLM providers (OpenAI, Anthropic, Azure, Google, Mistral, Groq, Ollama)

Usage example:
In Follow Up Prompts configuration, use {session_history} to access
the full conversation:

"Based on the current response: {history}

Previous conversation:
{session_history}

Generate 3 follow-up questions that haven't been asked yet."

Testing:
- Existing prompts using only {history} work unchanged
- New prompts using {session_history} receive formatted conversation history
- Works across all supported LLM providers
Fixes FlowiseAI#5868

The Agentflows page previously used client-side filtering which only
searched within the currently loaded page. If a target agentflow existed
on a different page, it would not be found.

Changes:

Backend (packages/server):
- Add optional 'search' parameter to getAllChatflows service
- Implement case-insensitive LIKE search across name, category, and id
- Use TypeORM Brackets for proper OR grouping in WHERE clause
- Pass search parameter from controller to service

Frontend (packages/ui):
- Remove client-side filterFlows function
- Add 300ms debounced search with setTimeout
- Reset to page 1 when search term changes
- Pass search parameter to API call
- Remove .filter(filterFlows) from card view
- Remove filterFunction prop from table view

Benefits:
- Search now works across all pages, not just current page
- Debouncing reduces API calls during typing
- Case-insensitive search for better UX
- Consistent behavior between card and table views
- Pagination resets to page 1 on new search

Testing:
- Create multiple agentflows across multiple pages
- Search for agentflow on page 2 while viewing page 1
- Verify search finds and displays the correct results
- Verify debouncing works (no API call on every keystroke)
- Verify pagination resets to page 1 on search
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces server-side search for chatflows, improves Docker security by adopting non-root user practices, and integrates session history into follow-up prompt generation. It also adds URL validation for tool icons and pagination for chat messages. Review feedback identifies a pagination indexing bug in the message utility, a lack of character escaping in SQL search queries, and the use of non-idiomatic React patterns for debouncing. Furthermore, the reviewer suggests using .replaceAll() to ensure all template placeholders are correctly substituted.

createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
}
},
skip: page > -1 && pageSize > -1 ? page * pageSize : undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The pagination logic here assumes 0-indexed pages (page * pageSize), but the rest of the application (including handleFeedbackQuery in this same file and getAllChatflows in the service layer) uses 1-indexed pages. If a user requests page 1, this implementation will skip the first page entirely. It should be updated to (page - 1) * pageSize to maintain consistency and correctness.

Suggested change
skip: page > -1 && pageSize > -1 ? page * pageSize : undefined,
skip: page > 0 && pageSize > 0 ? (page - 1) * pageSize : undefined,
References
  1. Maintain consistency with existing code patterns, even if they are not considered best practice.

Comment on lines +192 to +194
qb.where('LOWER(chat_flow.name) LIKE LOWER(:search)', { search: `%${search}%` })
.orWhere('LOWER(chat_flow.category) LIKE LOWER(:search)', { search: `%${search}%` })
.orWhere('LOWER(chat_flow.id) LIKE LOWER(:search)', { search: `%${search}%` })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When using the LIKE operator, special characters such as % and _ in the user's search input should be escaped to prevent them from being interpreted as wildcards. Additionally, applying LOWER() to chat_flow.id is redundant for UUIDs and can hinder index usage. Use simple chained .replace() calls for escaping to maintain readability and avoid complex regex.

                    const escapedSearch = search.replace('%', '\\%').replace('_', '\\_')
                    qb.where('LOWER(chat_flow.name) LIKE LOWER(:search)', { search: '%' + escapedSearch + '%' })
                        .orWhere('LOWER(chat_flow.category) LIKE LOWER(:search)', { search: '%' + escapedSearch + '%' })
                        .orWhere('chat_flow.id LIKE :search', { search: '%' + escapedSearch + '%' })
References
  1. Prioritize code readability and understandability over conciseness. A series of simple, chained operations can be preferable to a single, more complex one (e.g., a complex regex with a replacer function) if it improves understandability and reduces the potential for future errors.

Comment on lines +95 to +98
if (window.searchTimeout) clearTimeout(window.searchTimeout)
window.searchTimeout = setTimeout(() => {
refresh(1, pageLimit, agentflowVersion, searchValue)
}, 300)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Attaching a timeout to the window object is not idiomatic in React and can lead to side effects or conflicts if multiple components use the same property name. It is better to use a useRef hook to persist the timeout ID across renders without triggering re-renders or polluting the global namespace.

Comment on lines +35 to +37
let followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replace('{session_history}', sessionHistory)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The .replace() method only replaces the first occurrence of a substring. If the prompt template contains multiple placeholders for {history} or {session_history}, only the first one will be substituted. Using .replaceAll() ensures all instances are correctly replaced.

Suggested change
let followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replace('{session_history}', sessionHistory)
let followUpPromptsPrompt = providerConfig.prompt.replaceAll('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replaceAll('{session_history}', sessionHistory)
}
References
  1. Prioritize code readability and understandability over conciseness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client-side search on Agentflows page only searches current page

1 participant