Commit Graph

17895 Commits

Author SHA1 Message Date
e136bc7e70 chore(triggers): silence SQLModel exec-warning on legitimate execute() calls
SQLModel's AsyncSession raises a DeprecationWarning on every
session.execute() call urging the caller to use session.exec() —
but exec is typed for SELECT-returning statements only. Our five
execute() call-sites all pass non-SELECT statements that exec
explicitly does not accept (text() raw SQL or update() constructs),
so the warning is informational and cannot be acted on by changing
the code.

In the worker's hot path the warning fires on every claim cycle
(every 0.5–5s depending on backoff), flooding the log. The crud
helpers fire it once at boot (reset_stalled_in_progress) and once
per flow save (cancel_queued_jobs_for_components). None are
actionable, all are noisy.

NEW services/triggers/_sqlmodel_compat.py
  suppress_sqlmodel_exec_warning() — a narrow context manager that
  filters DeprecationWarning ONLY from sqlmodel.ext.asyncio.session.
  Any other DeprecationWarning still surfaces. The module is
  underscore-prefixed to signal it's an internal compatibility shim,
  not part of the public surface.

Five call-sites wrapped:
  worker.py (3): _claim_one's Postgres FOR UPDATE SKIP LOCKED select,
                its follow-up UPDATE, and the SQLite optimistic
                UPDATE...RETURNING.
  crud.py (2):  cancel_queued_jobs_for_components and
                reset_stalled_in_progress, both update() constructs.

Verified by running the full trigger test suite with
-W 'error::DeprecationWarning:sqlmodel.ext.asyncio.session' — all
tests still pass, confirming the suppression actually engages at
runtime (without it, the warnings-as-errors filter would fail every
test that hits the worker path). Ruff clean on all three files.
2026-05-21 15:09:04 -03:00
ed9fedfa28 fix(triggers): use absolute import for constants module
The component scanner (lfx.interface.components._process_single_module)
discovers a component class, instantiates it, and then re-evaluates the
class's *source code* via lfx.custom.validate.create_class to extract
its frontend template. That re-evaluation runs the module body in a
flat namespace — there is no package context, so relative imports
('from .constants import …') resolve to a non-existent top-level
'constants' module and raise ModuleNotFoundError.

The exception is swallowed silently inside _process_single_module
(it appends to a per-module failed_count and continues), so the
scanner returns an EMPTY component dict for the triggers package and
the entire category disappears from the frontend palette — exactly
the symptom: build_component_index / LFX_DEV scans complete with no
'triggers' key in the result.

Fix: switch to absolute import (lfx.components.triggers.constants).
Mirrors the convention used by every other built-in component;
relative imports of sibling helpers are not safe in this codebase
because of how the validator re-evaluates the source.

Verified via _process_single_module('lfx.components.triggers.cron_trigger'):
returns ('triggers', {'CronTrigger': <template>}) — the category now
surfaces to the frontend. 63/63 unit tests still pass.
2026-05-21 14:55:42 -03:00
d153be2801 fix(triggers-ui): register Triggers category in the canvas sidebar
The backend already exposed the new 'triggers' module (lfx/components/
triggers/) and the CronTrigger component within it, but the canvas
sidebar still rendered nothing for it. The reason: the categories
visible in the component palette are not auto-derived from the
backend's module list — they come from the frontend constant
SIDEBAR_CATEGORIES in src/frontend/src/utils/styleUtils.ts, which acts
as both the allowlist and the ordering source for the palette.

One entry added there (name='triggers', icon='Clock', display_name
i18n key) plus the matching 'sidebar.category.triggers' string in
locales/en.json. Placed right after Input & Output because a trigger
is conceptually the upstream-most node in a flow — first thing a
user looks for when wiring a scheduled flow.

No backend change needed; the backend already advertises 'triggers'
as a module via lfx.components.__init__, which is exactly the key
SIDEBAR_CATEGORIES.name matches against.
2026-05-21 14:31:45 -03:00
74b25bd43d refactor(triggers-ui): list view aggregator with bulk delete
The page model flips from 'create/edit trigger entities here' to
'survey the CronTrigger components scattered across the user's
flows'. Creation lives in the canvas; this page surfaces the
resulting set with row-level and bulk-level delete actions.

NEW types.ts shape
  TriggerInstance       — one CronTrigger component per row, with
                          next_fire_at + last_finished_status from
                          the trigger_job queue join.
  TriggerJob, BulkDeleteSummary — match backend serialisation.

NEW utils/format.ts
  formatDateTime / relativeTimeFrom — small pure helpers shared by
  the table and the jobs drawer. No new dep introduced; both use
  the platform Date + Intl APIs.

API hooks reshaped
  use-get-triggers.ts        list aggregator (TriggerInstance[])
  use-delete-trigger.ts      one component, DELETE /triggers/{flow}/{component}
  use-delete-all-triggers.ts NEW — DELETE /triggers, returns summary
  use-get-trigger-jobs.ts    new param shape: flowId + componentId
DELETED
  use-get-trigger.ts, use-post-trigger.ts, use-patch-trigger.ts
  — endpoints removed server-side (creation is canvas-only).
index.ts trimmed accordingly.

UI components rewritten / extended
  TriggersTable.tsx          checkbox column with select-all
                             (true / false / 'indeterminate'),
                             flow + component_id grouped cell, cron
                             chip, timezone, relative next-fire,
                             coloured last-run status badge,
                             kebab menu (Open in editor, View jobs,
                             Delete).
  TriggerJobsDrawer.tsx      switched to (flowId, componentId)
                             params; header now shows both
                             flow_name and component_id.
  BulkDeleteConfirmDialog.tsx NEW — single confirmation dialog reused
                             for 'delete selected' and 'delete all'.
                             Backed by the existing Dialog primitive,
                             not BaseModal, to keep the surface small.
  index.tsx                  page wires selection state, picks
                             between deleteOne-loop (selected subset)
                             and deleteAll (entire set), shows the
                             bulk button only when triggers exist.
DELETED
  TriggerFormModal.tsx, CronScheduleField.tsx — obsolete with the
  canvas-first model.

i18n
  Trimmed obsolete keys (cron picker, form fields, create/edit/save),
  added: col.nextFire, col.lastRun, openInEditor, deleteSelected,
  deleteAll, bulkDelete* (title, body with interpolation, confirm,
  success summary, error). Bulk strings use react-i18next
  interpolation tokens.

Verification: tsc clean on triggers/ files (pre-existing project-wide
errors unchanged); biome clean on 11 trigger files; Vite production
build succeeds in 35s.
2026-05-21 14:28:28 -03:00
a2eae0381b feat(triggers): read-mostly API surface (aggregator + delete + bulk)
The frontend list view + bulk-delete needs a small HTTP surface over
the in-flow trigger model. This commit adds it in three slim layers:

NEW services/triggers/queries.py
  list_triggers_for_user(session, user_id) → list[TriggerInstance]
    Walks user-owned flows whose data is non-null, scans for
    CronTrigger nodes via discovery, joins each with its most-recent
    trigger_job rows. Output dataclass carries (flow_id, flow_name,
    component_id, cron_expression, timezone, max_attempts,
    next_fire_at, last_finished_status, last_finished_at) — enough
    for the table without further round-trips.
  O(F) flows + O(F) job batches. Volume bound by hand-edits in the
  canvas; query count traded for simplicity.

NEW services/triggers/removal.py
  remove_cron_trigger_node(flow_data, component_id)
  remove_all_cron_trigger_nodes(flow_data)
    Pure dict mutations — no DB. Return new flow_data plus a
    boolean / list of removed ids. Both prune edges referencing the
    removed nodes so the resulting graph is still valid.
  Routes invoke these, write the result back via the standard
  _patch_flow helper, which fires the lifecycle hook that cancels
  the queued trigger_job rows for the removed components. No
  manual cascade in the API layer.

REWRITTEN api/v1/triggers.py
  GET    /api/v1/triggers
            list[TriggerInstance] for the current user
  DELETE /api/v1/triggers/{flow_id}/{component_id}
            strip one node + 204; 404 when not found
  DELETE /api/v1/triggers
            bulk strip every CronTrigger node from every owned flow;
            returns {flows_updated, components_removed}
  GET    /api/v1/triggers/{flow_id}/{component_id}/jobs
            recent trigger_job rows for one component; optional
            ?status_filter, default limit 50

  No POST / PATCH / PUT — the canvas is the only editing surface.
  Routes are thin: ownership check + helper call + response shape.

NEW tests/unit/services/triggers/test_queries.py (6 tests)
  empty user, multi-flow aggregation, user isolation,
  next-fire computation, last-terminal computation, flows
  without data.

NEW tests/unit/services/triggers/test_removal.py (10 tests)
  single-remove: missing flow_data, missing component, id collision
  with non-CronTrigger node, multi-node selectivity, edge pruning,
  input immutability. Bulk-remove: empty case, multi-strip with
  edge pruning, missing edges, malformed nodes list.

53/53 tests pass, ruff clean. The /triggers route is now ready for
the frontend list view in Etapa G.
2026-05-21 14:28:28 -03:00
bf4a35a276 refactor(triggers): worker reads live config + flow-save reconciles queue
Two coupled changes that together complete the component-based
trigger model end-to-end on the backend.

NEW services/triggers/discovery.py — pure parsing layer
  find_cron_trigger_nodes(flow_data)       → raw node dicts
  find_cron_trigger_configs(flow_data)     → list[CronTriggerConfig]
  parse_cron_trigger_config(node)          → typed dataclass
The matching key is node.data.type == 'CronTrigger' (the immutable
component identifier), never the id prefix. Defaults / clamping /
JSON parsing for the payload field live here so the worker and the
lifecycle hook never duplicate that logic. Tolerates malformed nodes
silently rather than raising.

NEW services/triggers/tweaks.py — boundary glue
  build_simplified_request_kwargs(config, fire_time) → dict
Constructs the SimplifiedAPIRequest kwargs that simple_run_flow
expects, ensuring the fire_time tweak lands on the right component_id.
Isolated so the worker stays a queue-only module and the executor
stays trigger-agnostic.

NEW services/triggers/lifecycle.py — flow-save side effect
  reconcile_trigger_jobs_for_flow(session, flow)
Idempotent reconciliation: components new to the flow get an initial
queued job; components removed from the flow have their queued jobs
flipped to CANCELLED (history preserved); unchanged components are
left alone because the worker reads live config at dispatch and
picks up any in-canvas edits automatically. Invalid configs
(unparseable cron, unknown IANA) are logged and skipped — the flow
save itself is never blocked.

REFACTORED worker.py — flow_id + component_id throughout
  _ClaimedJob carries flow_id and component_id (was: trigger_id).
  _DispatchContext is a new dataclass returned by _load_dispatch_context,
    which now resolves (Flow, User, live CronTriggerConfig) in one
    short txn. Returns None when any leg is missing (component was
    removed, flow deleted, user gone) — finalize layer handles that
    cleanly via a None-tolerant ctx parameter.
  _enqueue_next_cron_job uses ctx.config so a cron edit between
    enqueue and finalize takes effect at the very next fire.
  Three context fields land in simple_run_flow's context dict
    (trigger_component_id, trigger_job_id, trigger_fire_time) for
    downstream observability — replaces the prior trigger_id pair.

REWIRED api/v1/flows_helpers.py — 1 import + 3 single-line additions
  Calls reconcile_trigger_jobs_for_flow right after session.flush()
  in each of _new_flow, _update_existing_flow, _patch_flow. Mirrors
  the existing webhook detection pattern.

STUBBED api/v1/triggers.py — empty router until Etapa F lands the
new aggregator endpoints; keeps the import chain compiling.

REPLACED tests/unit/services/triggers/test_worker.py — 9 tests for
the new flow_id/component_id worker shape (claim, finalize success,
retry budget, exhausted retry, ctx-less failure, context loader
including the trigger-was-removed branch).

NEW tests/unit/services/triggers/test_discovery.py — 13 tests over
all branches of find/parse (none, empty, malformed, mixed,
defaults, clamping, JSON variants).

NEW tests/unit/services/triggers/test_lifecycle.py — 6 tests over
reconcile (enqueue new, idempotent re-save, cancel removed, mixed
add+remove, invalid cron skipped, unknown timezone skipped).

37/37 tests pass; ruff clean across all new + edited files.
2026-05-21 14:28:28 -03:00
8f3636fde3 refactor(triggers): reshape trigger_job around (flow_id, component_id)
The schedule moved into flow.data as a CronTrigger node, so the
parallel 'trigger' registry table no longer has a job. This commit
removes it and re-keys the trigger_job work queue against the flow +
component pair that the worker uses to look up live config on every
dispatch.

Schema changes (new Alembic revision tj02b3c4d5e6, chains from
tg01a2b3c4d5):
  - DELETE FROM trigger_job (purge — never deployed externally)
  - Rebuild trigger_job via batch_alter_table:
      drop trigger_id FK + column + its indexes
      add flow_id (UUID, NOT NULL, FK flow.id CASCADE)
      add component_id (String, NOT NULL)
  - Recreate ix_trigger_job_status_scheduled_at on the new column set
  - Add ix_trigger_job_flow_id, ix_trigger_job_component_id
  - DROP TABLE trigger (and its four indexes)
  - On Postgres only: DROP TYPE trigger_type_enum

Downgrade restores the prior shape but does not restore data — there
are no synthesised trigger.id values to backfill with. Round-trip
upgrade → downgrade → upgrade verified on a fresh SQLite DB.

Model changes (model.py):
  - Drop TriggerBase, Trigger, TriggerType enum
  - Drop TriggerCreate, TriggerRead, TriggerUpdate (the table-backed
    schemas no longer have a target)
  - Reshape TriggerJobBase: drop trigger_id, add flow_id +
    component_id with docstring explaining the queue → flow.data
    indirection
  - Keep TriggerJobRead with the new column set so the API layer
    can serialise

crud.py rewritten to match the new shape:
  - list_jobs_for_flow(flow_id, *, component_id?, status?, limit)
  - cancel_queued_jobs_for_components(flow_id, component_ids) — the
    lifecycle hook will call this when a CronTrigger node disappears
    from flow.data
  - reset_stalled_in_progress kept (still the boot recovery hook,
    untouched semantics)

Models package __init__.py trimmed to re-export only TriggerJob.
The TriggerJob FK ON DELETE CASCADE on flow_id means deleting the
flow purges its queue rows for free — no manual cascade in app code.
2026-05-21 14:28:28 -03:00
18cd0c1aa5 feat(triggers): add CronTrigger component and triggers category
The trigger is now an in-flow component, not an external entity.
Dragging CronTrigger into a flow declares the schedule; removing it
removes the schedule. The component's class name ("CronTrigger")
is the immutable identifier persisted in node ids — pinned by a unit
test so any accidental rename surfaces as a test failure rather than
breaking every saved flow.

Files (all new):
  src/lfx/src/lfx/components/triggers/cron_trigger.py
      The Component class. Five inputs (cron_expression, timezone
      dropdown with combobox=True for free-text IANA, max_attempts
      with RangeSpec(1..10), payload JSON, worker-fed fire_time)
      and a single Message output. build_event() echoes the
      worker-injected fire_time when present and falls back to the
      current UTC instant for manual canvas runs — mirroring the
      Webhook component's 'inert outside the trigger path' contract.
  src/lfx/src/lfx/components/triggers/constants.py
      DRY landing pad for the curated IANA list, default cron, default
      timezone, default max_attempts, and the upper attempt limit.
      The worker and any future trigger consumer import the same
      constants, so a change here propagates everywhere.
  src/lfx/src/lfx/components/triggers/__init__.py
      Lazy import pattern identical to every other category.

Three single-line additions to src/lfx/src/lfx/components/__init__.py
register the new category in (1) _dynamic_imports, (2) __all__, and
(3) the TYPE_CHECKING block. No other existing file touched.

Tests (new): src/backend/tests/unit/components/test_cron_trigger.py
covers class identity, palette metadata, input names + defaults,
timezone dropdown contents, output shape, manual run path, worker
injection path, status message, and whitespace defensiveness.
10/10 pass; ruff clean.
2026-05-21 14:28:28 -03:00
5235276ce7 fix(triggers-ui): filter flow selector by current user ownership
Previous attempts (remove_example_flows query param, folder-scoped
filter) both failed because the AUTO_LOGIN query path in
flows.read_flows() returns the union of user-owned flows AND every
flow with user_id IS NULL — and seeded starter templates have
user_id = NULL by construction (initial_setup/setup.py line 737, the
FlowCreate that builds them never sets user_id).

The discriminator that always works is user_id == current_user.id.
Templates have user_id = NULL; user-created flows carry the
authenticated user's UUID. Comparing against userData.id from
useAuthStore drops every template in one pass with no race against
folder hydration.

This is a stopgap fix while the broader refactor moves trigger
creation into the flow canvas (CronTrigger component). The form
modal itself will be removed once the in-flow component lands —
keeping the dropdown correct in the meantime so the existing flow
is usable without confusion.
2026-05-21 14:28:28 -03:00
a0aade5722 fix(triggers-ui): scope flow selector to user-owned folders
Previous version called useGetRefreshFlowsQuery with
remove_example_flows: true. That filter is supposed to drop flows
owned by the system 'Starter Projects' folder, but in practice the
dropdown still surfaced ~25 seeded templates from across the user's
installation — confusing for the trigger workflow, where the user
only wants flows they actually own.

Fix: pair the flows query with useGetFoldersQuery (the home page's
own folder fetch) and filter client-side to flows whose folder_id is
in the set returned by GET /api/v1/projects/. That endpoint already
excludes the system 'Starter Projects' folder server-side (folders.py
line 212: 'projects = [p for p in projects if p.name !=
STARTER_FOLDER_NAME]'), so the resulting set is exactly the folders
the user can navigate to via the sidebar.

Each option in the dropdown now also shows the parent folder name as
a muted suffix so users with many folders can disambiguate flows
with the same name. Options are alphabetized.

Empty state inside the Select: 'No flows available. Create or import
a flow first.' instead of a silently empty list.
2026-05-21 14:28:28 -03:00
266fe74116 fix(triggers-ui): hydrate flows from backend + replace cron text input
Two usability fixes after first-pass review:

1. Flow selector was empty when /triggers was opened directly (without
   visiting the home page first). The page now calls
   useGetRefreshFlowsQuery({ get_all: true, remove_example_flows: true })
   which populates flowsManagerStore — the same hook the home page
   uses — so the dropdown lists real flows and the table can map
   flow_id → flow name in the same paint. The query is gated on
   flows === undefined to skip the round-trip when the store is
   already hydrated.

   The modal also fires the same hook (with enabled = open) as a
   defensive second source: if a future caller renders the modal
   outside of the triggers page, the selector still works.

2. The raw cron-string field was unusable for non-technical users.
   New CronScheduleField component exposes five modes:
     - Every N minutes (1, 2, 5, 10, 15, 20, 30)
     - Every N hours (1, 2, 3, 4, 6, 8, 12)
     - Daily at HH:MM (HTML time input)
     - Weekly on day-of-week at HH:MM
     - Custom (raw cron, for advanced users)
   It parses an incoming cron back into the closest mode (so editing
   an existing trigger preserves intent) and re-composes the cron
   on every change. A read-only preview shows the resulting cron
   string so the user always sees what is going to the backend.

No new dependency. Zero tsc errors on the new files; biome lint
clean on all 13 trigger UI files.
2026-05-21 14:28:28 -03:00
c474bc112b feat(triggers-ui): register /triggers route, sidebar nav, and i18n
routes.tsx                         mount <Route path='triggers'> under
                                     CollectionPage so /triggers lands on
                                     the new page.
  sideBarFolderButtons/index.tsx     add a 'Triggers' button to the
                                     existing SidebarFooter block, next to
                                     'My Files' and 'Knowledge'. Uses the
                                     'Clock' lucide icon. The button lives
                                     inside the existing ENABLE_FILE_
                                     MANAGEMENT gate to stay consistent
                                     with how other nav entries are
                                     conditionally rendered; the route
                                     itself is unconditional so direct
                                     URL navigation always works.
  locales/en.json                    full 'triggers.*' and one 'sidebar.
                                     triggers' key. Flat dot-notation
                                     matches the existing convention; the
                                     non-English locale files autoload
                                     translations from a separate
                                     pipeline.

Pre-existing TS errors in folderSidebarComponent are unrelated to this
change (lines 144 and 453 vs our additions around 388 and 525).
2026-05-21 14:28:28 -03:00
35824b4874 feat(triggers-ui): add triggers page, table, form modal, jobs drawer
Mirrors the deploymentsPage / knowledgePage layout conventions:
xl:container layout, SidebarTrigger, title row with primary action
on the right, table for the main list, and a right-side drawer that
shifts the page mr-80 when open.

  index.tsx             page wrapper, state for form + drawer
  TriggersTable.tsx     row per trigger: name, flow name (from
                        flowsManagerStore), cron in a <code> chip,
                        timezone, is_active badge (click to toggle),
                        kebab menu (view jobs, edit, delete). Empty
                        state with a clock icon and loading spinner.
  TriggerFormModal.tsx  Dialog + react-hook-form + zod. Fields:
                        flow_id (Select, disabled on edit), name,
                        cron_expression (text input + 5-field client
                        sanity check), timezone (Select with curated
                        IANA list + 'Other' free-text fallback,
                        defaults UTC), payload (JSON textarea, soft
                        validated), max_attempts (1..10), is_active
                        (Switch).
  TriggerJobsDrawer.tsx 80-col right-side drawer, polls jobs every
                        5s (matches the worker's idle backoff cap so
                        a fired job is visible within seconds).
                        Badge per JobStatus, attempt counter, the
                        error text for failed runs.

No new runtime dependency. All primitives come from
components/ui/* (shadcn) and we lean on the existing
flowsManagerStore for the flow selector. Toasts go through the
established useAlertStore (setSuccessData / setErrorData).
2026-05-21 14:28:28 -03:00
025304b355 feat(triggers-ui): add TanStack Query hooks for triggers API
Six hooks under controllers/API/queries/triggers/, mirroring the
deployments pattern: signatures, query-key tuples, onSuccess cache
invalidation, and the request-processor wrapping.

  useGetTriggers     — GET /api/v1/triggers (optional flow_id filter)
  useGetTrigger      — GET /api/v1/triggers/{id}
  usePostTrigger     — POST, invalidates useGetTriggers
  usePatchTrigger    — PATCH, invalidates useGetTriggers and useGetTrigger
  useDeleteTrigger   — DELETE, invalidates useGetTriggers
  useGetTriggerJobs  — GET /api/v1/triggers/{id}/jobs (limit + status filter)

No new runtime dependency added — every primitive is already in the
codebase (axios, TanStack Query, the URL constant map).
2026-05-21 14:28:28 -03:00
85deeec374 feat(triggers-ui): add TRIGGERS URL constant and domain types
Mirrors the backend Pydantic schemas (Trigger, TriggerCreate,
TriggerUpdate, TriggerJob) without bringing in any new dependency.
The TRIGGERS URL slot in controllers/API/helpers/constants.ts uses
the same shape as DEPLOYMENTS so getURL('TRIGGERS') composes the
full /api/v1/triggers path identically.
2026-05-21 14:28:28 -03:00
0fae9511c2 style(triggers): clear ruff findings on new modules
Cosmetic-only changes from running ruff --fix and a handful of manual
fixups:

  - D205: split multi-line docstring openers into single-line
    summaries followed by a blank line and the longer description.
  - SIM105: use contextlib.suppress(asyncio.TimeoutError) in
    _sleep_with_stop instead of try/except/pass.
  - RUF015: avoid list(...)[0] in the trigger_job assertion, bind
    once and index.
  - TC003: move uuid.UUID import in crud.py into the TYPE_CHECKING
    block since it is only referenced as a parameter annotation.

No behaviour changes. All 15 unit tests still pass and ruff is now
clean on every file touched by this feature branch.
2026-05-21 14:28:27 -03:00
296e945e06 test(triggers): add unit tests for scheduler + worker
15 tests covering:

  scheduler (pure functions, 8 tests):
    - 5-minute cron in UTC
    - 02:30 daily in America/Sao_Paulo
    - DST spring-forward in America/New_York: 02:30 local does not
      exist; result must shift to the next valid local time (6:30 or
      7:30 UTC depending on whether croniter picks 02:30 EST or
      03:30 EDT — both are correct, the test accepts either)
    - 09:00 Europe/London cron rolling into next day
    - bad cron expression rejected
    - unknown IANA timezone rejected
    - validate_trigger_config bundles both checks
    - omitted 'after' uses utcnow and returns a future datetime

  worker (in-memory SQLite via the conftest async_session fixture,
  7 tests):
    - claim picks the eligible row and flips it to in_progress with
      started_at populated
    - claim skips future-scheduled rows
    - claim returns None on empty queue
    - finalize_success marks the row completed, sets run_job_id, and
      enqueues exactly one next-fire row for recurring cron triggers
    - finalize_success does NOT enqueue when the trigger is inactive
    - finalize_failure with remaining budget enqueues a retry with
      attempt+1 and marks the original row failed with the error
    - finalize_failure at max_attempts enqueues only the next cron
      fire (a fresh attempt=1), never a retry

While writing the worker test, discovered that SQLAlchemy returns
raw hex strings (not UUID) for text()-based queries since the type
adapter is bypassed. Added _coerce_uuid in the worker so downstream
comparisons against ORM Uuid columns work uniformly.
2026-05-21 14:28:27 -03:00
f84f767c95 feat(triggers): wire trigger worker into FastAPI lifespan
One asyncio.create_task per worker process spawns the trigger worker
loop alongside the existing queue_service and sync_flows_from_fs
tasks. The shutdown path signals via the stop event AND cancels the
task — the event makes shutdown deterministic (the loop wakes from
idle backoff immediately), the cancel covers the case where the
worker is mid-iteration.

Startup also runs recover_stalled_jobs() once: any trigger_job rows
left in_progress by a prior crash get flipped back to queued without
consuming a retry attempt. Wrapped in a try/except so a failure to
recover never blocks startup — the worker would just skip those rows
on its first iteration.

Imports are local to the lifespan block so a startup that disables
the database (rare CLI paths) does not pull the worker module.
2026-05-21 14:28:27 -03:00
5ad0c9bd03 feat(triggers): add worker loop with claim, dispatch, finalize
The heart of the feature. One async task per process drains the
trigger_job queue and dispatches each row through simple_run_flow.

Transaction discipline — the property the whole design hinges on:
  * _claim_one: short txn. SELECT id FROM trigger_job WHERE
    status='queued' ... FOR UPDATE SKIP LOCKED on Postgres; optimistic
    UPDATE ... WHERE status='queued' on SQLite (atomic because SQLite
    serializes writes). One row per iteration.
  * _dispatch: no txn. Loads the flow + user in their own short
    sessions, then calls simple_run_flow OUTSIDE any open transaction.
    This is the only safe way to run Postgres-as-queue — long work
    never holds a row lock.
  * _finalize_success / _finalize_failure: short txn. Flips the row
    to completed/failed and enqueues either the next cron occurrence
    or a retry attempt — all in the same txn so the new queue row is
    visible to the next iteration immediately.

Retry policy: exponential backoff 2 ** attempt seconds, capped at
five minutes. When the retry budget is exhausted, recurring cron
triggers still get their next scheduled fire enqueued — a transient
outage does not stop the schedule permanently.

Cancellation: trigger_worker_loop(stop_event) honours both
stop_event.set() and asyncio.Task.cancel(). Idle backoff doubles up
to a 5-second cap when the queue is empty; error backoff is 10s.
Every exception except CancelledError is swallowed and logged — one
bad trigger never takes the worker down.

recover_stalled_jobs() is a one-shot startup helper that flips
in_progress rows older than a threshold (default 30 min) back to
queued without consuming a retry attempt, recovering from worker
crashes. Wired into lifespan in the next commit.
2026-05-21 14:28:27 -03:00
8439aacae6 feat(triggers): add CRUD endpoints under /api/v1/triggers
Adds the route surface defined in the design doc:

  POST   /api/v1/triggers                 → 201 TriggerRead
  GET    /api/v1/triggers[?flow_id=]      → list[TriggerRead]
  GET    /api/v1/triggers/{id}            → TriggerRead
  PATCH  /api/v1/triggers/{id}            → TriggerRead
  DELETE /api/v1/triggers/{id}            → 204
  GET    /api/v1/triggers/{id}/jobs       → list[TriggerJobRead]

Schemas (TriggerCreate, TriggerRead, TriggerUpdate, TriggerJobRead)
live inline with the table model alongside Trigger / TriggerJob —
same convention used by flow.model and variable.model.

Validation flow:
  - cron expression and IANA timezone go through validate_trigger_config
    from services.triggers.scheduler; failure → 400 with the offending
    value in the message.
  - max_attempts is bounded to [1, 10].
  - flow_id is checked for cross-user access and 404s (not 403),
    matching the flow read endpoints.
  - duplicate (user_id, name) at insert/update time → 409 on the
    uq_trigger_user_name unique constraint.

On a successful create, the route also inserts the first trigger_job
row with scheduled_at = next_fire_time_utc(...). The worker picks it
up from there; no separate scheduling pass is needed.

Auth is via the standard CurrentActiveUser dependency. Cross-user
access on every endpoint returns 404.
2026-05-21 14:28:27 -03:00
7e3af452d9 feat(triggers): add scheduler module with cron + timezone helpers
Pure-function module — no database, no I/O. Two responsibilities:

  validate_trigger_config(cron, tz)
      Used by the create/patch endpoints to fail fast with a useful
      400 message. Bundles cron validation (croniter.is_valid) and
      timezone validation (zoneinfo.ZoneInfo).

  next_fire_time_utc(cron, tz, after)
      Computes the next fire time in UTC. The cron expression is
      interpreted in the trigger's timezone; conversion to UTC
      happens only at the very end. This is the only correct order
      for DST handling — a 02:30 daily cron on America/New_York on
      the spring-forward Sunday correctly jumps to the next valid
      local time instead of producing a phantom 02:30.

Verified scenarios:
  * 5-minute cron in UTC.
  * 02:30 daily in America/Sao_Paulo (UTC-3, no DST).
  * 02:30 daily in America/New_York across a DST boundary.
  * 09:00 London cron after midday UTC rolls to the next day.
  * Invalid cron expressions and unknown IANA timezones both raise
    InvalidTriggerConfigError with the offending value in the
    message.
2026-05-21 14:28:27 -03:00
b18dd35bca chore(triggers): add croniter dependency to langflow-base
croniter (BSD-licensed, ~30KB, no transitive deps beyond
python-dateutil which the project already ships) is used to parse the
trigger cron expression and to compute the next fire time. The
constraint range >=2.0.0,<6.0.0 covers every croniter release the
ecosystem currently uses; uv.lock resolves to 5.0.1.

No new infrastructure is introduced — no Redis, no broker, no
scheduler library. croniter is purely an expression parser; the
scheduling itself lives in the triggers service in the next commit.
2026-05-21 14:28:27 -03:00
059cc8f14c feat(triggers): add Alembic migration for trigger and trigger_job tables
Chains from mb01b2c3d4e5 (current head). Creates trigger_type_enum
(values: cron) and reuses the existing job_status_enum for
trigger_job.status via create_type=False — the type is owned by the
original job migration, so we do not re-create it.

Idempotent via langflow.utils.migration.table_exists, matching the
style of every other migration in this codebase. Downgrade drops
trigger_job first (the FK-dependent child), then trigger, then the
new trigger_type_enum on Postgres only.

Verified upgrade → downgrade → upgrade round-trip on a fresh SQLite
database produces the expected tables, columns, indexes (composite
ix_trigger_job_status_scheduled_at for the worker's claim query),
and the uq_trigger_user_name unique constraint.
2026-05-21 14:28:27 -03:00
c2dae89071 feat(triggers): add Trigger and TriggerJob SQLModel definitions
Two new tables for the native triggers feature:

  trigger      — one row per cron schedule (flow_id, user_id, cron
                 expression, IANA timezone, payload, is_active).
                 Unique on (user_id, name) to mirror flow naming.
  trigger_job  — the work queue. Worker claims rows with
                 FOR UPDATE SKIP LOCKED (Postgres) or an optimistic
                 UPDATE WHERE status='queued' (SQLite).

trigger_job.status reuses the existing JobStatus enum, with
create_type=False on the SQLEnum column so the existing
job_status_enum Postgres type is shared rather than re-created.

trigger_job.run_job_id cross-links to the existing job table once
simple_run_flow records the actual graph run, so triggered runs land
in the same monitoring surface as user-initiated runs.

Indexes:
  - composite (status, scheduled_at) on trigger_job: backs the
    worker's hot claim query.
  - trigger_id on trigger_job: backs the per-trigger history query.
  - flow_id, user_id, is_active on trigger: standard lookups.

crud.py keeps the surface narrow — get_trigger, list_triggers,
list_trigger_jobs, plus reset_stalled_in_progress which runs once at
startup to recover from worker crashes (rows with started_at older
than a threshold are flipped back to queued without consuming an
attempt).
2026-05-21 13:51:01 -03:00
b5db6fd0b2 fix(memory-base): Product Review Improvements (#13248)
* fix(memory-base): rename tool method to "retrieve_memory" and pad trace status badges

- Rename MemoryBaseComponent.retrieve_data to retrieve_memory so the
  derived tool label in tool mode reads "Retrieving Memory" instead
  of "Retrieve Data". Output.name kept as "retrieve_data" to preserve
  saved flow edges.
- Add vertical padding to status badges in TraceAccordionItem and
  SpanDetail so the success/failure label has breathing room inside
  the badge.

Added guardrails to MB system prompt.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-21 15:20:22 +00:00
838175a132 fix(composio): handle Python keywords and non-identifier field names in action schemas (#13139)
* fix: coerce Message and Data to primitives before Composio API execution

* fix: coerce Message and Data to primitives before Composio API execution

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Fix Ruff line

* fix(composio): handle Python keywords and non-identifier field names in action schemas

* [autofix.ci] apply automated fixes

* fix(composio): remap Python keyword fields to original names in execute_act

* ruff changes fixed

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-21 13:14:31 +00:00
288bc8efe4 fix(ci): use local bundle wheels in nightly install tests (#13257) 2026-05-20 19:46:40 -07:00
2df2032443 fix: Expose Message connection handle on Run Flow's exposed text inputs (LE-1233) (#13180)
* fix: Expose input handle for run flow

* Update test_run_flow.py
2026-05-20 21:50:02 +00:00
8ee70279f8 feat(memories): surface memory config in summary card with popover (#13209)
* feat(memory): improve memory UI with empty states, toasts, and model filtering

* [autofix.ci] apply automated fixes

* fix: capture memory id/name before setTimeout to prevent stale closure null access

* error improvement and testcase addition

* fix: surface refresh errors, restore API error messages, flatten sidebar ternary, and add keyboard access to batch-size tooltip

* [autofix.ci] apply automated fixes

* biome fix

* fix types

* [autofix.ci] apply automated fixes

* fix: UI polish for Memories and Traces sidebar sections

* [autofix.ci] apply automated fixes

* feat(memories): surface memory config in summary card with popover details

* fix: replace magic index, template literals, and inline noops in Memories/Traces sidebar

* [autofix.ci] apply automated fixes

* ruff fix

* fix testcases

* improve testcase cov import clean up

* [autofix.ci] apply automated fixes

* fix import order

* accessibility fix

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
2026-05-20 20:16:25 +00:00
dfb34c7be4 fix(agent): scope chat history retrieval by flow_id to prevent cross-flow leak (#13087)
* fix(agent): scope chat history retrieval by flow_id to prevent cross-flow leak (#13059)

AgentComponent.get_memory_data filtered chat history by session_id only, so
the playground's default session names (e.g. "New Session 0") leaked
conversation history across unrelated flows whenever two flows happened to
use the same default name.

The fix replaces the ad-hoc MemoryComponent spawn (whose internal _vertex is
None and therefore cannot see the running flow's flow_id) with a direct
aget_messages call that passes flow_id from the agent's own graph context.
MemoryComponent.retrieve_messages is also updated to scope by self.graph.flow_id
when available, so the standalone Message History component does not have
the same leak.

Adds regression tests covering: UUID coercion of string flow_ids, isolation
of two flows that share a session name, graceful fallback when flow_id is
missing or non-UUID, untouched external-memory path, and filtering out the
current input message.

* fix(agent): preserve n_messages=0 disable-memory contract and apply scoping to Cuga

Review follow-up on #13087:

- AgentComponent.get_memory_data regressed n_messages=0: before this PR the
  call went through MemoryComponent.retrieve_messages which short-circuits to
  [] when n_messages==0. The direct aget_messages call lost that contract,
  because `if self.n_messages:` is falsy for 0 and `messages[-0:]` returns
  every message fetched under limit=10000.

- CugaComponent.get_memory_data had the same leak pattern (issue #13059)
  because it also spawned an ad-hoc MemoryComponent whose _vertex is None.

Extract aget_agent_chat_history(session_id, flow_id, context_id, n_messages)
into memory.py:
  * returns [] when n_messages == 0
  * coerces flow_id to UUID (gracefully falls back on invalid values)
  * applies n_messages slicing

Both AgentComponent and CugaComponent now route through it.

Tests:
  * aget_agent_chat_history: passes flow_id as UUID, short-circuits on
    n_messages=0, slices to most-recent N, returns all on n_messages=None,
    falls back to unscoped query on invalid flow_id.
  * AgentComponent integration: routes through helper with flow_id, filters
    out current input, n_messages=0 disables memory (asserts aget_messages
    is never awaited so a future regression resurfaces as a real DB call).
  * CugaComponent integration: scopes by flow_id, n_messages=0 disables
    memory. Module-import gated for envs without optional Cuga deps.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes

* Template update

* [autofix.ci] apply automated fixes

* Update .secrets.baseline

* [autofix.ci] apply automated fixes

* Update starter projects

* [autofix.ci] apply automated fixes

* Update .secrets.baseline

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* fix(memory): address PR #13087 review — symmetric flow_id, DESC fetch, loud fallback

Addresses the review comments on PR #13087:

I1 (symmetric flow_id handling): `store_message` now routes the
internal-memory write through `_coerce_flow_id_to_uuid(_safe_graph_flow_id(self))`
instead of accessing `self.graph.flow_id` directly, so an ad-hoc /
custom-subclass MemoryComponent without `_vertex` no longer crashes
on the write half before reaching the safe read half. Both calls share
a single `flow_id_scope` variable.

I2 (>10k row ordering bug): both `aget_agent_chat_history` and
`MemoryComponent.retrieve_messages` (internal-memory branch) now query
in `DESC` order with `limit=n_messages` (falling back to
`MAX_CHAT_HISTORY_FETCH_LIMIT` when no explicit limit is set), then
reverse to the caller's preferred order. The previous ASC + slice shape
returned the chronological FIRST window once sessions exceeded 10k
rows, silently serving stale history.

I3 (loud fallback observability): when `_coerce_flow_id_to_uuid` is
forced to return `None` for a malformed `flow_id`, the log call is now
`logger.error` (was `warning`) and carries a structured `event` tag
(`memory_flow_id_unscoped`). The unbounded-fetch ceiling-hit path
emits a parallel `memory_chat_history_limit_reached` warning. Both
events are explicit alert hooks for observability pipelines so a
regression cannot silently re-enable the cross-flow leak that motivated
the PR.

R1 (magic number): extracted `MAX_CHAT_HISTORY_FETCH_LIMIT = 10_000` as
a module-level constant; reused in both call sites and the test suite.

R2 (signature): tightened `_coerce_flow_id_to_uuid` and
`_safe_graph_flow_id` from `Any` to `str | UUID | None`; tightened
`aget_agent_chat_history`'s `flow_id` argument accordingly.

Tests: extended `test_memory_flow_id_scoping.py` with coverage for
each of the above — symmetric store/read flow_id (I1), DESC+reverse
ordering on retrieve_messages (I2), ceiling-warning emission (I2),
structured error log on flow_id fallback (I3), and explicit-limit
suppression of the warning. All 26 tests pass.

* [autofix.ci] apply automated fixes

* Template update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-20 19:46:51 +00:00
ec3aea7ff8 fix(extensions): discover editable bundles; clean up ticket refs and reload UX (#13219)
* fix(extensions): discover editable installs via langflow.extensions entry-point

`lfx extension list` was silently dropping bundles installed via `uv pip
install -e` / `pip install -e`. Editable installs surface only
`dist-info/` entries in `dist.files`, so the wheel-shaped scan in
`_distribution_manifest_path` finds no `extension.json`. The bundle
pyproject.toml comment already promised an entry-point fallback for this
case, but it was never implemented in discovery.py.

Add `_distribution_manifest_path_via_entry_points` that resolves the
package via `importlib.util.find_spec` (no module body execution) and
locates the manifest under the resulting package directory. The fallback
runs only when `dist.files` produces no manifest, so wheel installs are
unaffected.

Tests:
- editable distribution with dist-info-only files is discovered
- entry-point pointing at an unimportable module yields no record
- wheel-install path never consults find_spec (guards against
  re-importing every bundle package on startup)

* chore(extensions): drop internal ticket refs; relax reload --bundle and --all

Two related changes:

1. Strip internal `LE-NNNN` ticket references from the extension source,
   bundles, tests, and public docs. The references were not actionable
   outside the project and surfaced in user-visible CLI messages and the
   author guide.

2. Relax `lfx extension reload` CLI ergonomics now that local discovery
   (`discover_all_extensions`) gives us the install map without needing
   an HTTP list endpoint:

   - `lfx extension reload <ext_id>`: `--bundle` is now optional; when
     omitted, the bundle name is resolved from local discovery. Explicit
     `--bundle` still wins for cases where the local install isn't
     visible to the running server.
   - `lfx extension reload --all`: iterates over every locally-discovered
     bundle, POSTs reload to each, renders per-bundle status, and exits 1
     if any reload fails. Previously hard-errored as "not yet wired".
   - `--all` is mutually exclusive with a positional id / `--bundle`
     (exit 2 with a clear message).
   - Missing both `extension_id` and `--all` exits 2 pointing at both.

Updated `test_reload_requires_explicit_bundle` (which enforced the old
"--bundle is mandatory" behavior) to cover the new resolution paths.

* docs(bundle-api): changelog entries for editable-install discovery + reload CLI

Satisfies the BUNDLE_API surface-change gate for the editable-install
discovery fallback (entry-point lookup in discovery.py) and the relaxed
`lfx extension reload` CLI (`--bundle` optional, `--all` implemented).

No additional behavior change in this commit -- it only documents the
two changes already shipped in this branch.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
2026-05-20 18:10:59 +00:00
651d9e71a8 fix: coerce numpy scalars in Memory Base metadata to Python primitives (#13211)
* fix: coerce numpy scalars in Memory Base metadata to Python primitives

Chroma persists integer/float metadata as numpy.int64 / numpy.float64
scalars. When the Memory Base component is used as an Agent tool, the
LangChain tool-output path calls vars() on and iterates these values
during serialization, raising:

    TypeError("'numpy.int64' object is not iterable")
    TypeError('vars() argument must have __dict__ attribute')

Coerce metadata values (and the derived _score) to Python primitives in
_format_results so the emitted DataFrame is JSON-safe regardless of how
downstream consumers serialize it.

* [autofix.ci] apply automated fixes

* Update component_index.json

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-20 15:40:21 +00:00
4915126efb feat(i18n): globalization pipeline — frontend i18n with backend locale support (#12933)
* feat(i18n): globalization pipeline — frontend + backend i18n support

Squash of PR #12825 (feat/gp-frontend-i18n-batch-c) onto release-1.10.0.

- IBM Globalization Pipeline integration with GP REST API client
- react-i18next setup with 7 language support (en, fr, ja, es, de, pt, zh-Hans)
- Lazy-load non-English locale files via Vite dynamic imports
- Language selector in Settings > General page
- Backend: serve translated component metadata via Accept-Language header
- Translate canvas node field labels, outputs, templates on language change
- Translate note nodes via dedicated endpoint
- Translate all UI modals and pages (MCP, Knowledge Base, Settings, etc.)
- CI: GP upload/download workflows, auto-bake note keys
- Content-hash component keys for stable GP translations
- Preserve user-customized node display_name/description on language switch

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* revert: restore scripts/gp to release-1.10.0 state (no functional changes)

* revert: restore package-lock.json and flows_helpers.py to release-1.10.0

package-lock.json: npm noise, not part of this feature
flows_helpers.py: unrelated blank-line change

* revert: remove out-of-scope backend changes

- starter_projects/*.json: trivial trailing newline only, not i18n content
- api/v1/flows.py: unrelated deployment guard removal and flow update refactor
- tests/unit/test_i18n_note_translation.py: minor non-functional changes

* revert: restore Pokédex Agent.json to release-1.10.0

* fix(i18n): remove unused import and fix loop variable shadowing in i18n.py

Resolves ruff F401 (content_hash imported but unused) and PLW2901
(for-loop variable `node` overwritten by assignment).

* [autofix.ci] apply automated fixes

* fix(i18n): fix ruff I001 import sort in i18n.py

* fix: remove duplicate useTranslation hook declaration in recentFilesComponent

* fix: restore deployment components overwritten by squash merge and fix i18n test mock

The squash merge brought in older versions of deployment page components,
stripping functionality added by PRs that landed on release-1.10.0 after
our feature branch diverged (duplicate tool name validation, deploy choice
dialog update flow, step-type/step-attach-flows logic).

Restored 14 files to their release-1.10.0 state. Also added __esModule: true
to the i18n mock in flowStore.test.ts to fix the i18n_1.default.t TypeError.

* fix: restore addMcpServerModal, SaveSnapshotButton, KnowledgeBaseUploadModal to release-1.10.0

These files had functionality stripped by the squash merge (buildKeyPairPayload/
buildArgsPayload helpers, save version dialog logic, modal height/validation
constants). Restored to release-1.10.0 state.

* fix: restore flowSidebarComponent ShadTooltip wrappers lost in squash merge

* fix(i18n): revert node_copy rename back to translated_node in translate_flow_notes

The rename was unnecessary — release-1.10.0 already used translated_node
(not reassigning the loop variable node), so ruff PLW2901 doesn't apply.

* chore: sync frontend locale files from GP [skip ci]

* chore: sync backend locale files from GP [skip ci]

* chore: sync backend locale files from GP [skip ci]

* fix(i18n): add missing en.json keys and sync frontend locale files from GP

Added 7 missing keys: alerts.modelsRefreshed, mcp.servers.toolsCount,
misc.fetchErrorDescription/Message, misc.timeoutErrorDescription/Message,
store.results.

* [autofix.ci] apply automated fixes

* ci: add frontend i18n key coverage check with PR report

Adds a script and GitHub Actions workflow that:
- Scans all .ts/.tsx source files for t("key") calls
- Verifies every key exists in en.json
- Posts a markdown report as a sticky PR comment explaining:
  - Missing keys (actionable failures)
  - The expected gap between en.json total and detected keys
    (pluralization suffixes, dynamic keys, test files excluded)

Fails the check if any hardcoded t("key") call has no en.json entry.

* [autofix.ci] apply automated fixes

* feat(i18n): translate remaining hardcoded trace column headers and sync locale files

* fix: resolve ruff style check failures in check_frontend_i18n_keys.py

- Use set membership for multiple comparisons (PLR1714)
- Replace ambiguous Unicode character with plain text (RUF001)
- Split long lines to comply with 120 char limit (E501)
- Replace magic number with named constant (PLR2004)

* [autofix.ci] apply automated fixes

* fix(i18n): narrow zh-* mapping to Simplified Chinese variants only

zh-TW and zh-HK (Traditional Chinese) now fall back to English instead
of incorrectly receiving Simplified Chinese (zh-Hans) translations.

* fix(i18n): use canonical folder names instead of translated strings in use-add-flow

Folder names are stored identifiers used by backend MCP setup and
useGetFolders — translating them caused non-English users to get
unrecognized folder names, breaking starter-project flows and MCP wiring.

* [autofix.ci] apply automated fixes

* fix(i18n): use hardcoded English name for new project to avoid translated MCP server names

* fix(i18n): truncate long sidebar labels and show full text in tooltip

Long translated strings (e.g. Japanese) were wrapping inside the
fixed-height get-started panel. Labels now truncate with ellipsis
and reveal the full text via ShadTooltip on hover.

* [autofix.ci] apply automated fixes

* feat(i18n): translate Deploy button label and sync locale files

* fix(i18n): sync SUPPORTED_LANGUAGES with actual locale files

Remove ru and ko (no locale files exist) and add de (de.json exists).

* fix(i18n): fall back to English for null and empty string translations

Add returnNull and returnEmptyString options so partially translated
locale files with null or empty placeholder values still show English.

* fix(i18n): prevent crash on Chinese browser locale by routing through normalizeLanguage

index.tsx had its own detectedLang using navigator.language.split("-")[0],
which stripped "zh-CN" to "zh" and bypassed normalizeLanguage, causing
loadLanguage to attempt importing a non-existent zh.json and crash.

Now index.tsx imports detectedLang from i18n.ts so all locale detection
goes through normalizeLanguage — zh/zh-CN/zh-TW all resolve safely.

* feat(i18n): translate knowledge base table headers, status labels, and context menu

- Wire useTranslation into FilesPanel (Sources label, file/files count)
- Pass t() into createKnowledgeBaseColumns for all column headers and menu items
- Convert STATUS_CONFIG labels to i18n keys so Ready/Ingesting/etc. translate
- Upload updated en.json to GP and download machine translations for all 6 locales

* feat(i18n): translate run component tooltip and export modal button

* feat(i18n): wrap hardcoded strings with t() across deployment stepper, flow version sidebar, knowledge base, and account pages

Adds 53 new translation keys to all 7 locale files and wraps previously hardcoded English strings with t() calls in 17 source files: deployment stepper components (connection search, connection panel, flow list panel, step-type, step-review, success content, footer, delete dialog), flow version sidebar (save dialog, snapshot button, sidebar, delete confirm), knowledge base drawer and chunk card, delete account page, and store API key form.

* feat(i18n): translate edit node modal, table modal, and AG Grid strings

Wraps hardcoded strings in editNodeModal (Field Name, Description, Value,
Show, Expose Input, Close), textAreaModal (Finish Editing), intComponent
placeholders, tableModal Save button, ColumnConfig Open Table button, and
AG Grid built-in overlays (No Rows To Show, pagination strings) with t().

* feat(i18n): translate playground chat, file management, user modal, notifications, deployment dialogs, crash page, and toolbar shortcuts

Wraps hardcoded strings with t() across ~50 files and adds ~156 new keys to all 7 locale files (en, de, es, fr, ja, pt, zh-Hans).

Areas covered:
- Playground: sessions sidebar, voice input tooltips/errors, chat input placeholder, drop zone, file attachment tooltip
- File management: context menu (rename/download/duplicate/delete/remove), confirmation modals, CRUD notifications
- User management modal: username, password, confirm password, active, superuser fields
- Notifications panel: title
- MCP server modal: ja.json fix for Streamable HTTP/SSE URL field label and placeholder
- Messages table: "files" column header
- Deployment dialogs: Select Deployment phase, Flows step (available/attached/removed badges, connection panel, flow list panel)
- Crash error page: all text regions including split description around GitHub Issues link
- Toolbar shortcuts: shortcutDisplay renders translated name via shortcuts.name.* keys (keys already existed in all locales)

* feat(i18n): translate node toolbar action tooltips, update bar labels, and confirmation modal strings

* feat(i18n): translate note toolbar, inspection panel, sidebar config trigger, and table pagination strings

* feat(i18n): translate header GitHub/Discord tooltips, playground disabled tooltip, and session options tooltip

* feat(i18n): translate IOModal new chat, edit/copy message, and helpful/not-helpful tooltips

* feat(i18n): translate voice assistant audio settings, labels, tooltips, and API key inputs

* translations of previous entries

* feat(i18n): translate hardcoded strings across error messages, JSX text, and aria/title attributes

Migrated ~55 new keys to en.json and updated ~45 source files: error toasts (streaming,
build failures, file ops, shortcuts), modal titles (session logs, flow details, view text,
restore version, CSV separator, chunk preview), node/output labels (incompatible with, no
output, component output, no tools), model panel (configure provider, not enabled), JSON
editor validation messages, Langflow logo title attributes, and various aria-labels.

* feat(i18n): translate remaining hardcoded strings in alt texts, placeholders, and step titles

- Add useTranslation + translate alt text in ProfileIcon, assistant-empty-state, CanvasControls, assistant-message, assistant-no-models-state
- Translate assistant welcome text and suggestion labels in assistant-empty-state
- Fix dropdownComponent search placeholder to use existing input.searchOptions key
- Fix mcp-server-notice image alt text
- Fix empty-page Langflow logo alt texts (light/dark)
- Add useTranslation + translate file upload step titles in code-tabs via STEP_TITLE_KEYS lookup map
- Add 9 new keys to en.json: sidebar.mcpNoticeImageAlt, common.langflowLogoLight/Dark/userProfileAlt, assistant.welcomeText, assistant.suggestion.*, apiModal.uploadFilesStep/executeFlowStep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(i18n): translate remaining hardcoded strings across modals, panels, and components

- codeAreaModal: Done button, Check & Save, Discard Changes, Caution confirmation
- shareModal: Share header, Replace confirmation, status public label, Export button, footer labels, success message, attention text
- mustachePromptModal: Edit Prompt title, Prompt Variables label, Check & Save button
- updateComponentModal: Standard label, backup checkbox, update button, warning paragraphs
- update-phase: Close/Test buttons, sr-only dialog title/description strings
- provider-phase: Cancel/Continue buttons, provider description
- assistant-component-result: Inputs/Outputs headings, Approved/Add to Canvas/View Code actions
- message-options: Edit message, Helpful, Not helpful, Copied, Copy message tooltips
- storeCardComponent: Private, Components, Likes, Downloads, Like, Install Locally, error liking
- ProviderList: Loading providers text
- outputModal: Outputs/Logs tabs, inspect description
- io-field-view: No node found, Enter text placeholders
- InspectionPanelFields: No editable fields, No advanced settings
- InspectionPanelOutputs: No output data message
- FlowVersionSidebar: No saved versions yet
- step-attach-flows-version-panel: Select flow/version, loading/no versions, Created date
- StepReview: No files, generating preview, could not generate, Summary, labels, not selected
- VisibilityToggleButton: Hide field / Show field aria-labels
- ModelSelection: Disable/Enable model aria-labels
- MemoizedSidebarTrigger: Components label
- file-card, file-preview: File label

Adds 63 new keys to en.json (1,500 total).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(i18n): add missing useTranslation hook to SidebarTrigger component

SidebarTrigger used t("ui.toggleSidebar") without calling useTranslation(),
causing a ReferenceError crash when the TemplatesModal opened (which renders
a SidebarTrigger with no children prop, hitting the sr-only span).

* feat(i18n): translate visibility tooltips, connect other models, global variables placeholder, and status filter truncation

- Translate 4 visibility toggle tooltips in tableAdvancedToggleCellRender
- Fix "Connect other models" always showing English by using t() directly instead of backend display_name fallback
- Translate Global Variables search placeholder in inputGlobalComponent
- Add SelectTrigger truncation for status filter dropdown in FlowInsightsContent

* feat(i18n): resolve ModelSelection conflict — merge Deprecated badge with translated aria-labels

Combines changes from both branches:
- Keep translated aria-labels for enable/disable model toggles using existing keys
- Add Deprecated badge from release-1.10.0 with t("modelProvider.deprecated")
- Add modelProvider.deprecated key to en.json

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* feat(i18n): fix missing en.json keys and translate hardcoded strings across Knowledge Base, Memory, and component files

Adds 53 new translation keys (knowledge.*, memory.* namespace — new, and misc) and wires up t() calls in 19 source files to eliminate hardcoded English strings in placeholders, labels, modal titles, aria-labels, and empty states.

* chore: remove check-frontend-i18n CI workflow

* chore: remove check_frontend_i18n_keys.py script

* [autofix.ci] apply automated fixes

* chore: revert component_index.json version bumps from release-1.10.0 merge

* chore: sync component_index.json to release-1.10.0

* feat(i18n): update backend locale translations from GP

* fix(i18n): fix frontend jest test failures from i18n migration

- Fix paginator empty state: strip "of " from translation keys and
  add it inline in the non-empty branch so 0 items shows "0 items"
  instead of "of 0 items"
- Revert text renames back to original English copy: "Ingest Content",
  "Add Files", "DB Provider", "1 VERSION", and the full deploy-from-draft
  message so existing tests keep passing without modification
- Fix punctuation mismatches: remove trailing "!" from changesSaved and
  "..." from loadingProviders
- Add i18next plural form resolution (_one/_other) to the jest t() mock
  so pluralised strings resolve correctly in tests
- Default t param in createKnowledgeBaseColumns to en.json fallback
  instead of key passthrough so the function works correctly without
  a translation function argument

* test(frontend): fix deploy version assertion

* fix(i18n): add missing useTranslation import to MemoizedComponents

* translation updated

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>
2026-05-20 13:38:53 +00:00
f1ecb51319 fix: UI polish for Memories and Traces sidebar sections (#13205)
* feat(memory): improve memory UI with empty states, toasts, and model filtering

* [autofix.ci] apply automated fixes

* fix: capture memory id/name before setTimeout to prevent stale closure null access

* error improvement and testcase addition

* fix: surface refresh errors, restore API error messages, flatten sidebar ternary, and add keyboard access to batch-size tooltip

* [autofix.ci] apply automated fixes

* biome fix

* fix types

* [autofix.ci] apply automated fixes

* fix: UI polish for Memories and Traces sidebar sections

* [autofix.ci] apply automated fixes

* fix: replace magic index, template literals, and inline noops in Memories/Traces sidebar

* [autofix.ci] apply automated fixes

* ruff fix

* fix testcases

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
2026-05-19 23:47:16 +00:00
4f9ef2af37 refactor: Move File System component to Files & Knowledge category (#13223)
* change fs category

* component index

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-19 23:26:54 +00:00
cb10f1d10f docs: file system component (#13183)
* docs-file-system-component

* delete-internal-feature-doc

* docs-filesystem-component

* fix-links

* Apply suggestions from code review

Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com>

* clarify-metadata

* fix-merge

---------

Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com>
2026-05-19 18:46:30 +00:00
bd54c1b952 feat(memory): improve memory UI with empty states, toasts, and model (#13116)
* feat(memory): improve memory UI with empty states, toasts, and model filtering

* [autofix.ci] apply automated fixes

* fix: capture memory id/name before setTimeout to prevent stale closure null access

* error improvement and testcase addition

* fix: surface refresh errors, restore API error messages, flatten sidebar ternary, and add keyboard access to batch-size tooltip

* [autofix.ci] apply automated fixes

* biome fix

* fix types

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
2026-05-19 18:33:51 +00:00
bc656b44e8 fix(openrouter): validate against /api/v1/auth/key and disconnect all variables (#13214)
Two follow-ups to PR #13185 surfaced during release-1.10.0 QA.

BUG-1 (high) — OpenRouter API key validation never rejected invalid keys.
``validate_model_provider_key`` probed OpenRouter's ``/api/v1/models``
endpoint, which is *public* and returns 200 for any bearer token (including
missing/invalid). So ``POST /api/v1/variables/`` accepted any string as an
OPENROUTER_API_KEY with 201, the catalog swapped to the live 356-model list,
and runs failed at request time. Route the probe through
``/api/v1/auth/key`` instead — auth-required, returns 401 on invalid, the
existing ValueError → HTTP 400 mapping in ``api/v1/variable.py`` does the
rest. Same fix lifts BUG-3 since the generalized validation path is shared.

BUG-2 (medium) — clicking Disconnect on OpenRouter in Settings → Model
Providers did nothing. ``handleDisconnect`` resolved the variable name via
the static ``PROVIDER_VARIABLE_MAPPING`` constant, which doesn't include
"OpenRouter" (and the constant is deprecated in favour of the dynamic
``GET /api/v1/models/provider-variable-mapping`` API). Source the variable
keys from ``providerVariables`` instead and delete every configured one in
parallel so multi-variable providers (OpenRouter's API key + attribution
headers, IBM WatsonX's apikey + project_id + url) are fully removed. The
static mapping is kept as a fallback for the brief window before the
provider-variable API resolves.

Tests
- ``test_openrouter_provider.py``: update happy/regression cases to pin the
  new ``/api/v1/auth/key`` URL and add an explicit assertion that the public
  ``/api/v1/models`` endpoint is never used for validation.
- ``test_variable.py``: add ``POST /api/v1/variables/`` integration tests
  covering both a 401 (returns 400 with "Invalid OpenRouter API key") and a
  200 (returns 201), and assert the auth-required URL is used.
- ``useProviderConfigurationDisconnect.test.tsx``: new suite — disconnect
  for OpenRouter deletes all three variables in one batch, fall-back to the
  static map for Anthropic still works, no-ops when nothing is configured,
  surfaces an error toast when a delete fails.

Fixes the issues reported against release-1.10.0 OSS QA of PR #13185.
2026-05-19 12:27:12 -07:00
09bd68d349 docs: text operations component (#13152)
docs-add-text-operations-component
2026-05-19 18:25:50 +00:00
5a8c17b22d ci: backport bundle/nightly CI fixes from main (#13206, #13207, #13208) (#13210)
* ci: stop publishing -nightly bundle variants; release bundles on demand (#13206)

Bundles (lfx-arxiv, lfx-duckduckgo) change infrequently enough that a
nightly cadence is overkill. Drop the rename-to-`-nightly` and bundle
publish lanes from the nightly pipeline and add a dedicated
workflow_dispatch-only release_bundles.yml for purposeful releases.

- nightly_build.yml: drop update_bundle_versions.py invocation and the
  src/bundles/*/pyproject.toml git-add guard.
- release_nightly.yml: remove build-nightly-bundles and
  publish-nightly-bundles jobs; untether test-cross-platform and
  publish-nightly-main from them.
- release_bundles.yml (new): build all src/bundles/* wheels, run a
  cross-OS install smoke test, then publish to PyPI under their stable
  names. Tolerates already-published versions so re-runs without a
  version bump are no-ops.

release.yml still owns bundle publishing as part of main releases.

Manual follow-up (PyPI admin): delete the lfx-arxiv-nightly and
lfx-duckduckgo-nightly projects from PyPI.

* ci(release_bundles): build lfx wheel locally for smoke test (#13207)

The release_bundles.yml smoke test installs bundle wheels into a fresh
venv, which makes uv resolve their `lfx>=X.Y,<Z` pin against PyPI. When
bundles are released ahead of a matching lfx (typical case — bundles
ship more often than lfx bumps land on PyPI), the resolve fails:

  Because only lfx<=0.4.3 is available and lfx-arxiv==0.1.0 depends on
  lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv==0.1.0 cannot be
  used.

Build the lfx wheel from the current ref in the build-bundles job and
install it alongside the bundle wheels in the smoke test. The lfx
wheel ships as a separate `dist-lfx-smoketest` artifact and is NOT
included in the publish-bundles step — only the `dist-bundles`
artifact gets pushed to PyPI.

* ci(nightly): re-pin bundle lfx deps to lfx-nightly during nightly build (#13208)

The nightly pipeline renames the workspace `lfx` package to `lfx-nightly`
in `src/lfx/pyproject.toml` and the root workspace dep, but leaves each
`src/bundles/*/pyproject.toml`'s `"lfx>=X.Y,<Z"` pin unchanged. With the
workspace `lfx` gone and PyPI still on lfx 0.4.3, the create-nightly-tag
job's `uv lock` fails:

  × No solution found when resolving dependencies for split [...]
  ╰─▶ Because only lfx<=0.4.3 is available and lfx-arxiv depends on
      lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv's requirements
      are unsatisfiable.

`update_lfx_version.py` now also rewrites the `lfx` (or already-rewritten
`lfx-nightly`) specifier in every `src/bundles/*/pyproject.toml` to
`lfx-nightly==<dev version>`, mirroring how `update_lf_base_dependency`
re-pins the lfx dep inside `langflow-base`. The lookahead in the regex
prevents matching sibling packages like `lfx-arxiv` / `lfx-duckduckgo`.

No-op when no bundle pyprojects exist (e.g. on main today), so this is
safe to merge ahead of the bundle extraction landing on main. Restored
the guarded `git add src/bundles/*/pyproject.toml` step so the modified
bundle pyprojects ride the same commit/tag as the rest of the nightly
version bumps.

Cherry-pick to release-1.10.0 to unblock nightlies cut from that branch.
2026-05-19 10:12:54 -07:00
feec57e16f docs: merge knowledge base components (#13150)
* docs-merge-kb-components

* Apply suggestions from code review

Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com>

* use-tabs-for-params-table

---------

Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com>
2026-05-19 16:23:22 +00:00
b9e6fff677 feat: Redesign deployment environment cards (#13109)
* feat(deployments): redesign env cards

* refactor(deployments): split env ui

* test(e2e): fix provider delete selectors
2026-05-19 16:13:26 +00:00
aefd8acb37 docs: directory component is legacy (#13186)
* remove-and-redirect-component-page

* update-tutorials-that-used-directory-component

* peer-review

* update-tutorial-screenshot
2026-05-19 15:33:50 +00:00
487a2402ba fix: Model handling for tool calling in agents and update IBM models (#13201)
fix: Model handling for tool calling in agents and update IBM models (#13191)

* Model handling for tool calling in agents and update IBM models

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-19 08:02:35 -07:00
ae45d3ad8e fix: File upload toast showing only after successful upload (#12984)
* File upload toast showing only ater successful upload

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-19 12:21:07 +00:00
2be85b167e feat: promote OpenRouter to a first-class unified model provider (#13185)
* feat: promote OpenRouter to a first-class unified model provider

Adds OpenRouter to the unified Model Providers system so users can
configure it once in Settings rather than per-flow. Reuses ChatOpenAI
with base_url=https://openrouter.ai/api/v1, supports a live model
catalog (fetched from /api/v1/models with the user's bearer key), and
forwards the optional HTTP-Referer / X-Title attribution headers when
the user provides a site URL or app name.

Validation is a cheap GET against /api/v1/models that raises
ValueError("Invalid OpenRouter API key") on a 401. The existing
flow-level OpenRouterComponent is left untouched for backward
compatibility.

Implements the opinionated direction proposed in #12839 (single
curated provider instead of a generic OpenAI-compatible field).

* Update Structured Data Analysis Agent.json

* [autofix.ci] apply automated fixes

* fix(openrouter): address review feedback

- Per-model tool_calling derived from OpenRouter's `supported_parameters`,
  not blanket-True. The Agent component calls
  `get_language_model_options(tool_calling=True)` so this fixes a real bug
  where non-tool-calling models (e.g. perceptron/perceptron-mk1) would have
  appeared in agent flows and failed at runtime.
- Default-model picks intersect with the curated seed list instead of
  taking the first 5 alphabetical ids (which were ai21/jamba-large-1.7,
  aion-labs/aion-1.0, etc.). Falls back to first MIN_DEFAULT_MODELS when
  the seed list shares nothing with the live catalog (stale-seed safety).
- Broadened the live-fetch except to include ValueError/TypeError (covers
  malformed JSON, non-list `data`, non-dict entries) and added defensive
  isinstance checks so a 200 with a bad payload degrades to `[]` instead
  of crashing on AttributeError.
- Bumped the live-fetch failure log from `debug` to `warning` with URL +
  status code, so an empty live catalog leaves a server-side breadcrumb.
- Wrapped the credentials probe in try/except → ValueError so a 5xx /
  timeout during `POST /validate-provider` surfaces as a user-facing 400
  via api/v1/variable.py (which only catches ValueError), not an
  unhandled 500.
- Tightened `os.environ.get(var["variable_key"])` (was
  `var.get("variable_key", "")` → `os.environ.get("")` masking misconfig).
- Documented `is_header` / `header_name` and the settings-only
  (no-`component_metadata`) pattern in the MODEL_PROVIDER_METADATA header
  comment.
- Updated seed slugs against the current OpenRouter catalog
  (anthropic/claude-3.7-sonnet was missing); curated list is now
  Claude Opus/Sonnet/Haiku 4.x + GPT-4o pair + Gemini 2.5 Pro + Llama 3.3.
- Added OPENROUTER_API_KEY / OPENROUTER_SITE_URL / OPENROUTER_APP_NAME to
  VARIABLES_TO_GET_FROM_ENVIRONMENT so users with env vars set see the
  provider as configured in Settings → Model Providers (parity with the
  other providers, and makes the "Falls back to env" copy honest).
- Tests: pinned `MIN_DEFAULT_MODELS` instead of magic 3; added cases for
  per-model tool_calling derivation, seed-intersection defaults,
  stale-seed fallback, httpx.HTTPStatusError, malformed payload, transient
  network → ValueError validation, env-var attribution headers, and env
  auto-import list. Replaced `patch.dict("sys.modules", ...)` with
  `patch.object(requests, "get", ...)` so the test stays correct if the
  lazy import is hoisted.

* Update Structured Data Analysis Agent.json

* feat(model-providers): search bar, models.dev metadata override, capability tags

Three follow-ups to the OpenRouter PR, requested by @ogabrielluiz in Slack:

1. **Search bars in the model providers modal.** ProviderList accepts a
   `query` prop and filters case-insensitively on the provider name; an
   `<Input icon="Search">` in ModelProvidersContent drives it. Inside the
   per-provider panel, ModelSelection grows its own search input that
   filters both LLM and embedding lists and resets when the selected
   provider changes (so OpenRouter → Anthropic doesn't carry stale text).
   Empty-state copy in both lanes; new i18n keys.

2. **models.dev metadata override (source of truth for covered providers).**
   New `lfx.base.models.models_dev_catalog` module:
     * `fetch_models_dev_snapshot()` — httpx GET against
       https://models.dev/api.json, returns None on transport / status /
       payload errors with a warning log.
     * `load_models_dev_snapshot()` / `save_models_dev_snapshot()` — disk
       cache at `<user_cache_dir>/langflow/models_dev/snapshot.json` with
       atomic temp-file write.
     * `apply_models_dev_overrides()` — translates each covered provider's
       models.dev entries into `ModelMetadata` (tool_call → tool_calling,
       reasoning passthrough, modalities.input has "image" → vision,
       limit.context → context_window, cost.input/output →
       cost_per_million_in/out). Providers not covered (IBM WatsonX, Azure
       OpenAI, Groq) pass through unchanged. Live-fetched providers
       (Ollama, IBM WatsonX, OpenRouter) keep their behavior because
       `replace_with_live_models` runs downstream at read time.
   `provider_queries.get_models_detailed()` now consults the in-memory
   snapshot via `get_active_snapshot()` and is `cache_clear()`-friendly so
   snapshot installs are picked up on the next call. The non-blocking
   refresh task in `get_lifespan` hydrates from disk first, then attempts a
   live fetch on a 24h interval; startup never blocks on models.dev.

3. **Capability tags next to the model name.** ModelRow renders small
   Badge pills (tool / reasoning / vision / embedding / search / preview)
   driven by metadata flags, in a stable left-to-right order, with the
   existing Deprecated badge kept at the end. The embedding tag only
   surfaces in the `all` view because the dedicated embeddings section
   header already conveys the same info.

Tests:
  - 14 new backend cases in `test_models_dev_catalog.py` (fetch / load /
    save / override / cache invalidation paths).
  - Search + capability-tag cases added to ProviderList and ModelSelection
    Jest suites (44 total, all green).
  - lfx tests/unit/base/models + tests/unit/inputs: 114 pass, no
    regressions.
  - ruff check clean on all touched files.

Out of scope: manual UI refresh button, surfacing pricing in the UI,
auto-adding providers from models.dev without a LangChain client.

* fix(openrouter): derive reasoning from supported_parameters

OpenRouter exposes "reasoning" in each model's supported_parameters array
exactly like it does "tools" (verified against /api/v1/models — Claude
Opus 4.x, o1/o3, DeepSeek R1 all carry it). The live fetcher was only
deriving tool_calling; reasoning was left at the create_model_metadata
default of False, so the new reasoning badge never lit up for OpenRouter
models even though the rest of the wiring was in place.

Now derives both flags from the same array. Test renamed to
…_derives_tool_calling_and_reasoning_per_model and pins all four
combinations (tools+reasoning, tools only, reasoning only, neither, plus
the missing-supported_parameters case).

* feat(model-providers): sort by recency, hide deprecated by default

OpenAI's list looked jumbled because the catalog rendered in raw
constants-file order — reasoning models interleaved with chat models,
deprecated and preview rows scattered throughout. Three changes:

1. **Thread `created` (Unix epoch) through ModelMetadata.** New optional
   field; populated by:
     - OpenRouter live fetch (existing `created` field on every model;
       defensive int-coercion handles bad payload values).
     - models.dev override (parses `release_date` YYYY-MM-DD → UTC
       midnight epoch; invalid / missing → 0).
   Static `*_constants.py` lists leave it at 0 → "unknown" tier.

2. **Sort each provider's group in `get_unified_models_detailed`.**
   Stable sort by `(is_deprecated, -created_epoch)`:
     - Non-deprecated dated rows: newest first.
     - Untimed rows (current static fallback): stable sort keeps their
       hand-curated constants-file order intact within the same tier.
     - Deprecated rows: bottom.
   The "first 5 = default" stamping now runs AFTER the sort so the
   highlighted defaults reflect the new ordering. For OpenAI's static
   list this changes nothing today (constants are already newest-first);
   once models.dev hydrates it picks up genuinely-recent dates from
   `release_date`.

3. **Collapse deprecated into a `<details>` disclosure.** Each section
   renders active rows normally, then a `Show N deprecated model(s)`
   summary that's closed by default. Resets on provider change. The rows
   stay in the DOM so existing flows referencing deprecated models still
   resolve; they're just out of the way.

`not_supported` is already filtered at the API layer (the modal doesn't
pass `includeUnsupported`), so those rows remain hidden as before.

Tests:
  - Backend: `created` propagation from `release_date` (+ malformed/empty
    cases), OpenRouter `created` passthrough (+ invalid-value
    degradation), and a full sort-order assertion covering
    [newest, mid, undated-a, undated-b, deprecated] ordering.
  - Frontend: deprecated disclosure default-closed, singular pluralization,
    no-disclosure-when-nothing-deprecated, reset on provider change via
    real user click → onToggle (the right pattern; the prior draft of
    this test mutated `details.open` directly and missed React's prop
    reconciliation).
  - 117 lfx tests + 48 frontend tests pass; ruff clean.

* [autofix.ci] apply automated fixes

* fix(models.dev override): preserve static deprecated flag + auto-deprecate dated snapshots

Two issues surfaced in the OpenAI/Anthropic screenshots:

1. **gpt-3.5-turbo wasn't in the deprecated disclosure** even though
   openai_constants.py has it flagged ``deprecated=True``. models.dev
   exposes no deprecated/lifecycle field (verified: only
   ``release_date`` and ``last_updated``), so the override threw away
   our static curation. ``apply_models_dev_overrides`` now builds a
   provider→{deprecated model names} lookup from the static lists and
   forwards the flag through ``_translate_model_entry`` for any name
   that matched a static deprecated row. ``gpt-4.5-preview`` and
   ``gpt-4-turbo-preview`` ride the same path.

2. **Anthropic dated-snapshot ids** (``claude-opus-4-5-20251101``,
   ``claude-haiku-4-5-20251001``, etc.) crowded the picker alongside
   their moving aliases. These are pinned-date snapshots of the
   alias — useful for production pins, noisy as defaults. Same
   pattern exists for OpenAI (``gpt-4o-2024-05-13``).
   ``_translate_model_entry`` now matches ``-YYYYMMDD$`` (Anthropic)
   and ``-YYYY-MM-DD$`` (OpenAI) and auto-flags those rows deprecated
   so they collapse into the disclosure tier rather than dominating
   the list. The 4-digit-only ``-0314``-style suffixes
   (``gpt-4-0314``, ``gpt-4-0613``) are deliberately *not* matched —
   those are pre-dated-naming snapshots that need static curation if
   we want to mark them.

Tests:
  - test_apply_overrides_marks_dated_snapshot_ids_deprecated pins:
      * alias rows stay active (claude-opus-4-5, gpt-4o)
      * Anthropic 8-digit and OpenAI hyphenated suffixes flip to
        deprecated
      * 4-digit-only suffixes don't false-positive
  - test_apply_overrides_preserves_static_deprecated_flag pins:
      * gpt-3.5-turbo / gpt-4.5-preview keep their static deprecation
      * a new model not in the static list (gpt-6) stays active

* fix(models.dev override): classify embeddings + age-based auto-deprecation

Two follow-ups from screenshot review on the OpenAI provider list.

**Bug 1 — embedding models showed in the Language Models section.**
models.dev mixes embeddings into a provider's ``models`` dict (e.g.
``text-embedding-3-large`` lives in the OpenAI block). The translator
was defaulting every entry to ``model_type="llm"``, so embeddings
polluted the LLM lane in the modal. The clean discriminator is the
``family`` field (``"text-embedding"`` for OpenAI's current embeddings,
verified for ada-002 / 3-small / 3-large); a name-substring check
("embedding") covers providers that fill ``family`` inconsistently.

**Bug 2 — old OpenAI models (gpt-4, gpt-4-turbo, text-embedding-ada-002)
stayed active even though they're functionally legacy.** models.dev has
no deprecation field, and OpenAI doesn't formally deprecate gpt-4 /
gpt-4-turbo in their catalog, but at ~924 days from release they're
clearly out of rotation. New ``_is_aged_out`` helper auto-deprecates
models older than ~30 months (900 days), preferring ``release_date``
over ``last_updated`` (the latter tracks catalog-curator edits like
typo fixes, not new model versions — gpt-4 has last_updated=2024-04
which would falsely keep it active).

Threshold tuned conservatively against today's catalog:
  - 1250d ada-002 → deprecated
  - 924d gpt-4, gpt-4-turbo → deprecated
  - 844d text-embedding-3-large/small → ACTIVE (still OpenAI's current
    embeddings; threshold deliberately above this point)
  - 735d gpt-4o → active
  - 261d Claude 4.x → active

``now`` is injected through ``_translate_model_entry`` and
``apply_models_dev_overrides`` for testability so the unit tests don't
depend on wall-clock time.

Tests:
  - test_apply_overrides_marks_embedding_family_as_embeddings_model_type
    pins ``family="text-embedding"`` → embeddings, generic LLMs stay
    llm, and a name-substring case (``voyage-3-embedding``) still
    classifies correctly when family is missing.
  - test_apply_overrides_auto_deprecates_stale_models pins both sides
    of the 900-day line and the no-date case (insufficient signal →
    stays active).

* fix(models.dev override): drop duplicate static groups + Google dated-preview + gemini-1.5

Three issues from screenshot review.

**1. Duplicate embedding rows in the OpenAI section.** The override
iterates ``static_lists`` and replaces the first OpenAI group
(``OPENAI_MODELS_DETAILED``, LLMs) with the combined override (which
holds both LLMs and embeddings from the single models.dev block). The
second OpenAI group (``OPENAI_EMBEDDING_MODELS_DETAILED``) then fell
through to the else branch and got appended unchanged, so every
embedding row rendered twice. Fix: when a provider's override has
already been applied, drop any subsequent static groups for the same
provider — they would only re-introduce duplicates.

**2. Google's dated-preview suffix wasn't auto-deprecated.** Each
vendor uses a different shape for snapshot ids:

| Vendor    | Shape                | Example                                   |
| --------- | -------------------- | ----------------------------------------- |
| Anthropic | ``-YYYYMMDD``        | ``claude-opus-4-5-20251101``              |
| OpenAI    | ``-YYYY-MM-DD``      | ``gpt-4o-2024-05-13``                     |
| Google    | ``-preview-MM-DD``   | ``gemini-2.5-pro-preview-05-06``          |
| Google    | ``-preview-MM-YYYY`` | ``gemini-2.5-flash-preview-09-2025``      |

``_DATED_SNAPSHOT_RE`` extended to match the Google variants. The
non-dated ``-preview-tts``-style suffix stays active (tts is a real
variant, not a snapshot).

**3. gemini-1.5-* stayed active despite being long out of rotation.**
At ~600-820 days these names sit under the 900-day age threshold, so
the heuristic alone wouldn't catch them. Added explicit
``deprecated=True`` entries for ``gemini-1.5-pro`` /
``gemini-1.5-flash`` / ``gemini-1.5-flash-8b`` to
``google_generative_ai_constants``; the override's static-deprecation
preservation flows them through.

Tests:
  - test_apply_overrides_drops_subsequent_static_groups_for_overridden_provider
    pins the no-duplicates invariant.
  - test_apply_overrides_marks_dated_snapshot_ids_deprecated grows to
    cover the Google preview-MM-DD / preview-MM-YYYY patterns and the
    non-dated -preview-tts negative case.
  - test_apply_overrides_preserves_gemini_1_5_static_deprecation pins
    that the static curation actually reaches the override output
    (and includes a sanity assert on the static list itself, so we
    don't drift the test off of the constants file by accident).

57 lfx model tests pass; ruff clean.

* [autofix.ci] apply automated fixes

* fix(ollama): stop 10s frontend poll + parallelize /api/show + cache

Closes #13137.

Two layered fixes for the runaway Ollama model fetch:

**Frontend — drop the 10-second refetchInterval.** The previous code
polled ``/api/v1/models`` every 10s as long as the Ollama provider card
was selected. Each backend call fanned out to ``GET /api/tags`` + ``POST
/api/show`` per model serially, so with many local models the request
overran the 10s interval and requests accumulated. The catalog already
refreshes on credential save / disconnect via
``invalidateProviderQueries`` and the backend cache (below) keeps a
on-demand window-focus refresh cheap; the timer was unnecessary.

**Backend — parallelize ``/api/show`` and cache.**

* ``get_ollama_models`` now fans out per-model capability probes via
  ``asyncio.gather``, so total latency is bounded by the slowest single
  request rather than ``N * avg``. Per-model failures are absorbed at
  debug-level — one broken model no longer poisons the whole catalog
  response.
* In-process TTL cache keyed by ``(base_url, capability)`` with a 30s
  window in front of the fan-out. Overlapping ``/api/v1/models``
  requests (which happen on every modal interaction, agent picker,
  embed picker, etc.) now share one upstream round-trip. Test-only
  ``_ollama_cache_clear`` helper for isolation.

Tests:
  - test_get_ollama_models_returns_only_matching_capability — capability
    filter still works.
  - test_get_ollama_models_runs_show_requests_in_parallel — pins
    parallelism via an asyncio.Event that would deadlock under serial.
  - test_get_ollama_models_tolerates_single_show_failure — one bad model
    doesn't drop the whole list.
  - test_get_ollama_models_caches_result_for_ttl_window — second call
    within the TTL hits the cache (zero upstream calls).
  - test_get_ollama_models_cache_keyed_by_base_url_and_capability —
    different base URLs and capability filters stay isolated.
  - test_get_ollama_models_refetches_after_ttl_expires — TTL expiry
    triggers a fresh fetch.

* Update Structured Data Analysis Agent.json

* [autofix.ci] apply automated fixes

* feat(model-input): declarative filters propagate to picker + sticky-default

Agent's Language Model dropdown was still showing tool-incompatible
models like ``gemini-3.1-flash-image-preview`` (``tool_call=false``
per models.dev). Root cause: the tool-calling constraint lived as a
hardcoded lambda at the Agent component's call site, but
``update_model_options_in_build_config``'s sticky-default
re-injection path didn't know about it — so a previously-saved
selection that no longer passed the filter got re-injected with
``not_enabled_locally=True`` and kept appearing in the picker.

Make the filter declarative so the helper, the cache key, and the
sticky-default path all read it from the same source.

* ``ModelInputMixin`` gains a ``filters: dict[str, Any] | None`` field.
  Any keys/values forward to ``get_unified_models_detailed`` and are
  matched against each model's metadata. Example: the Agent declares
  ``ModelInput(name="model", filters={"tool_calling": True})``
  instead of passing a lambda at the call site.

* ``get_language_model_options`` accepts ``filters: dict | None``;
  the legacy ``tool_calling`` kwarg is preserved and merged into the
  filter dict for backward compatibility.

* ``handle_model_input_update`` reads ``filters`` from the
  build_config and, when no explicit ``get_options_func`` is supplied,
  builds one that applies them. The options-cache key prefix is
  namespaced by sorted filter pairs so different constraint sets
  don't share cached options.

* The sticky-default block in ``update_model_options_in_build_config``
  validates the saved selection against the same filters dict via
  ``_saved_model_passes_filters``. When the saved row doesn't satisfy
  every constraint, the row is dropped and the value cleared so the
  downstream auto-default picks a compatible model. Conservative
  fallback: an unknown model (not in the catalog) still passes the
  filter so today's "inject and let the user configure" UX is
  unchanged for genuinely-unknown selections.

* Agent component: drop the inline lambda in ``update_build_config``;
  the filter now lives on the ModelInput declaration.

* Replaces the ``cache_key_prefix`` sniff for tool-calling intent —
  prefix is now derived from filters, not pattern-matched.

Tests cover the helpers, both sticky-default branches (drop vs
inject), and the conservative fallback for unknown saved models.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix(google): tool_calling=False on image-output Gemini models

Even after the declarative filters={"tool_calling": True} shipped on
the Agent component's ModelInput, the picker still showed
gemini-3.1-flash-image-preview and friends. Root cause: the static
google_generative_ai_constants.py catalog defaulted every Gemini
model to tool_calling=True. Image-generation models can't actually
run with tools (verified in models.dev — these rows have
tool_call: false and modalities.output includes image), so the
static-only fallback (before the models.dev refresh hydrates) was
wrong upstream of the filter and leaked them through.

Four image-output models flipped to tool_calling=False in the static
catalog:
  * gemini-2.5-flash-image
  * gemini-2.0-flash-preview-image-generation
  * gemini-3-pro-image-preview
  * gemini-3.1-flash-image-preview

Models.dev's correct flag still wins via the override path once the
snapshot hydrates; this fix matters most for cold starts and
air-gapped deployments where only the bundled static lists are in
play.

Tests (test_static_catalog_tool_calling.py):
  - Parametrized over the four known image models; each must declare
    tool_calling=False in the static catalog (and remain listed — we
    need the static curation so the override preserves the flag by
    name match).
  - Heuristic regression guard: any Google model with "image" in its
    name that defaults to tool_calling=True will fail this test.
    Future image variants are caught before they reach the picker;
    legitimate exceptions opt in via an allowlist literal in the test.

* [autofix.ci] apply automated fixes

* fix(model-input): source filters from class declaration, not round-tripped build_config

The Agent picker was STILL showing gemini-3.1-flash-image-preview even
after the static catalog flag flip and the declarative filters work.
Reproduced and traced to a saved-flow round-trip: flows persisted
before the filters field shipped don't carry it in their stored
template. The frontend POSTs that stale template back, my helper read
build_config["model"]["filters"] (None), and the filter path
short-circuited to the unfiltered branch.

Fix: source filters from the ModelInput's class-level declaration
(canonical, always current with the server build), falling back to
build_config only when the component happens to expose no inputs
attribute. The build_config is also patched in place when the
declaration overrides it, so the next round-trip carries the active
filter back to the frontend.

* New helper ``_filters_from_component_inputs`` walks the component's
  inputs list and returns the matching ModelInput's filters dict (with
  None values dropped, same hygiene as the build_config path).
* New ``_resolve_filters`` orchestrates: class declaration > build_config,
  with an in-place write back to build_config when the class wins.
* Both call sites in ``handle_model_input_update`` and
  ``update_model_options_in_build_config`` now call ``_resolve_filters``;
  the old direct call to ``_filters_from_build_config`` is gone from the
  hot paths (kept exported because it's still useful as a defensive
  fallback when no component is available).

Tests (4 new in test_build_config_sticky_default.py):
  - _filters_from_component_inputs reads class declaration / returns
    empty when no matching input.
  - _resolve_filters prefers class over build_config and patches
    build_config in place.
  - _resolve_filters falls back to build_config when component has no
    inputs attribute (defensive fallback for non-standard callers).

* fix(frontend): honor ModelInput.filters in dropdown augment loop

The Agent picker kept showing tool-incompatible models like
gemini-3.1-flash-image-preview even after every backend filter and
static-catalog fix landed. Root cause was on the frontend, in the
ModelInputComponent's groupedOptions memo:

  - The first loop iterates over the backend-supplied options (which
    are filtered).
  - The augment loop then iterates over every enabled model in
    useGetModelProviders × useGetEnabledModels — both of which
    are filter-unaware — and pushes the union into the dropdown.

So even though the backend correctly excluded
gemini-3.1-flash-image-preview from options, the augment loop happily
re-added it because providersData listed it and enabledModelsData
flagged it as enabled.

Fix: read filters off nodeClass.template.model.filters (the same
declaration that drives the backend filter) and apply the same metadata
match to every option that flows through groupedOptions. Two
checkpoints:

  1. Defensive pass in the existing-options loop. The backend should
     have filtered already, but stale saved flows could surface a
     build_config that hasn't been corrected yet — same self-healing
     story as the backend _resolve_filters helper.
  2. New pass in the augment loop, after the model_type check.
     Every enabled-model candidate must satisfy the filter or it's
     skipped.

Conservative behavior matches the backend: when a filter key (e.g.
tool_calling) is declared but the candidate's metadata doesn't
carry that key at all, the candidate fails the check — better drop an
undeclared row than surface one that crashes at run time.

Tests (4 new): hides tool-incompatible augmented models, drops
saved options that fail the filter, conservative drop on missing
metadata key (with a known-good sibling so the assertion isn't
paper-thin), no-op when no filters are declared. Suite-local
beforeEach restores the default hook mocks since jest.clearAllMocks
only resets call history, not implementations.

* Fix tests

* Update .secrets.baseline

* Fix starter projects

* Update .secrets.baseline

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update component_index.json

* Update Structured Data Analysis Agent.json

* [autofix.ci] apply automated fixes

* fix frontend tests

* fix remaining CI errors

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-18 21:39:06 -07:00
c901602bc2 fix(ci): rename bundles to -nightly during nightly build (#13193)
fix: rename bundles for nightly build
2026-05-18 18:32:44 -07:00
23f2edf327 chore: Clean up the startup warnings for Python 3.14 (#13156)
* fix: backport policies ToolGuard lazy imports (#13144)

fix: backport policies toolguard lazy imports

* chore: Clean up the startup warnings for Python 3.14

* Update base.py

* Update component_index.json

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update component_index.json

* [autofix.ci] apply automated fixes

* Update Structured Data Analysis Agent.json

* [autofix.ci] apply automated fixes

* Update component_index.json

* [autofix.ci] apply automated fixes

* Update component_index.json

* Update starter projects

* Update .secrets.baseline

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-18 16:52:13 -07:00
e08080507b feat: Add langflow-stepflow package (#12015)
* feat: Add langflow-stepflow package

Introduces a new workspace package `src/langflow-stepflow/` that ports
the Stepflow integration from the Stepflow repository into the Langflow
codebase.

The package has two submodules:
- `translation/`: translates Langflow flow JSON to Stepflow flow
  definitions (ported from integrations/langflow/converter/)
- `worker/`: a Stepflow worker that executes individual Langflow
  components via lfx (ported from integrations/langflow/executor/)

Entry point: `python -m langflow_stepflow.worker` starts the HTTP worker
server for use by a Stepflow orchestrator.

No changes to existing Langflow code in this commit.

* fix: Address review feedback on langflow-stepflow package

- Widen Python version to >=3.10,<3.14 to match langflow-base
- Fix import sorting (ruff I001) and unused variable (ruff F841)
- Replace sk- prefixed test strings to avoid Gitleaks false positives
- Remove placeholder step reference resolution in component_tool
  (orchestrator resolves these before reaching the worker)
- Let exceptions propagate from component_tool_executor instead of
  returning error dicts that look like successful results
- Skip secret/password field defaults in tool input schemas
- Use LRU-style bounded cache (128) for compiled components
- Use asyncio.to_thread for sync component methods to avoid blocking
- Fix mutable NamedTuple default (list→tuple) in PlaceholderGraph
- Warn and skip deps with missing field mappings instead of silently
  falling back to "input" (which overwrites on multiple deps)
- Tighten _is_data_list to check __class_name__ == "Data" specifically
- Add comment explaining intentional teardown-before-init sequence
- Add integration test for example flows

* chore: Upgrade to Stepflow SDK 0.12.0 and adapt to lfx 0.3+ renames

- Bump stepflow-py and stepflow-orchestrator from >=0.10.0 to >=0.12.0
- Remove Python 3.11 environment markers (SDK now supports 3.10+)
- Replace server.run() with gRPC pull transport (run_grpc_worker)
  matching the upstream stepflow-langflow integration pattern
- Update Flow serialization from Pydantic model_dump to msgspec.to_builtins
- Remove Pydantic ValueExpr/actual_instance unwrapping (now plain dicts)
- Add _langflow_type_name() to map lfx 0.3+ class renames (JSON→Data,
  Table→DataFrame) back to canonical Langflow type names
- Fix DataFrame isinstance checks for lfx Table subclass
- Add missing README.md, tests/__init__.py, helpers package
- Add skip conditions for missing fixture files

* change to lfx schema

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* chore: Align langflow-stepflow ruff config with repo and format

- Set line-length = 120 to match root pyproject.toml (was 88, causing
  conflicts between pre-commit format hook and check hook)
- Run ruff format across all source and test files
- Add pragma: allowlist secret on test fixtures with fake API keys

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* chore: Rebuild component index

* [autofix.ci] apply automated fixes

* fix(langflow-stepflow): allow Python 3.14 in requires-python

Lift requires-python from <3.14 to <3.15 to match the rest of the
workspace and the lockfile. Without this, uv sync on Python 3.14
fails the Unit/LFX/Integration test jobs and the Docker build.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: ogabrielluiz <gabriel@langflow.org>
Co-authored-by: Jordan Frazier <jordan.frazier@datastax.com>
2026-05-18 21:47:20 +00:00
d1da768192 feat(knowledge): merge ingestion + retrieval into mode-driven Knowledge component (#13120)
* feat(knowledge): merge ingestion + retrieval into mode-driven Knowledge component

Merge KnowledgeIngestionComponent and KnowledgeBaseComponent into a single
KnowledgeComponent driven by a TabInput mode selector ("Ingest" / "Retrieve"),
mirroring the PoliciesComponent pattern.

- New: src/lfx/src/lfx/components/files_and_knowledge/knowledge.py holds the
  full merged component. update_build_config and update_outputs hide the
  inputs and the output that do not apply to the current mode.
- Legacy KnowledgeIngestionComponent / KnowledgeBaseComponent become thin
  subclasses pinned to their respective modes with class-level inputs and
  outputs aligned to the legacy shape, so saved flows continue to load.
- Output names ("dataframe_output", "retrieve_data") preserved for back-compat.
- New tests cover mode-switch input/output visibility and subclass pinning;
  existing test_ingestion / test_retrieval suites rerouted to patch the new
  knowledge module path and continue to pass.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* chore(knowledge): drop emoji from mode labels, mark legacy subclasses

- Mode tab labels: "📥 Ingest" / "🔍 Retrieve" → "Ingest" / "Retrieve" so the
  selector blends with the rest of Langflow's TabInput palette (see MemoryComponent
  for the same convention).
- Set `legacy = True` on KnowledgeIngestionComponent and KnowledgeBaseComponent
  so the back-compat shims stay loadable by saved flows but no longer clutter
  the new-component palette — users should reach for the merged Knowledge
  component instead.

* [autofix.ci] apply automated fixes

* fix(knowledge): declare both outputs at class level so retrieve mode dispatches correctly

Saved flows in retrieve mode were hitting
``'NoneType' object has no attribute 'to_dataframe'`` because the merged
``KnowledgeComponent`` only declared ``dataframe_output`` (build_kb_info)
at the class level. The runtime resolves outputs from the class-level
``outputs`` list at __init__ time, so ``retrieve_data`` was never in
``_outputs_map``; the runtime fell back to ``build_kb_info``, which ran
``convert_to_dataframe(self.input_df)`` against a hidden, None-valued
input.

Fix: declare both outputs at the class level (TypeConverterComponent +
MemoryComponent already use this pattern). ``update_outputs`` keeps
filtering the canvas-visible output by mode, but the runtime can now
dispatch to either method based on which one the saved flow has wired up.
Also wire ``update_frontend_node`` to re-run the mode-driven output
filter on canvas load so saved flows render the correct output even
before the user clicks the tab.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix(knowledge): dispatch by mode in output methods to survive stale edges and legacy labels

Two failure modes were still triggering the
``'NoneType' object has no attribute 'to_dataframe'`` crash even with both
outputs declared at the class level:

1. User drops the Knowledge component (default INGEST), wires the
   ``dataframe_output`` edge, then switches to Retrieve. The saved edge
   still points at ``build_kb_info``; the runtime calls it against a
   None ``input_df`` and crashes inside ``convert_to_dataframe``.
2. Flows saved with the original emoji mode labels ("📥 Ingest" /
   "🔍 Retrieve") carry those strings on disk. The post-emoji-removal
   equality checks (``mode == MODE_RETRIEVE``) failed, so even though
   the user was conceptually in retrieve mode, the component fell
   through the ingest path.

Both methods now check the mode at entry and delegate to the other one
when the wired output doesn't match. Mode matching is lenient
(substring-based) so the legacy emoji labels still resolve.

``update_build_config`` and ``update_outputs`` also normalize legacy
labels onto the current canonical values so visibility toggles correctly
for flows pulled from old saves.

* [autofix.ci] apply automated fixes

* feat(knowledge): point legacy shims at the merged component and migrate starters

- Add ``replacement = ["files_and_knowledge.Knowledge"]`` to both
  ``KnowledgeIngestionComponent`` and ``KnowledgeBaseComponent`` so the
  canvas legacy-component banner offers the merged component as the
  one-click upgrade target.
- Rewrite the ``KnowledgeBase`` nodes in the two starter projects that
  shipped them (``Knowledge Retrieval`` and ``Vector Store RAG``) to use
  the new ``Knowledge`` component with mode pre-set to ``Retrieve``. All
  edges are re-pointed at the new node id, the embedded module path is
  updated to ``lfx.components.files_and_knowledge.knowledge.KnowledgeComponent``,
  and the mode value is normalised away from the old emoji label.

* [autofix.ci] apply automated fixes

* Update knowledge.py

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update component_index.json

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update src/lfx/src/lfx/components/files_and_knowledge/knowledge.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix: Restore knowledge creation functionality

* Template update

* Update component_index.json

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix: Linting errors

* Update knowledge.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes

* fix: Address @rafaelgiln comments

* Update test_knowledge.py

* Update component_index.json

* Template updates

* Update .secrets.baseline

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update Structured Data Analysis Agent.json

* Update component_index.json

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Fix edge connection with parser

* Update Structured Data Analysis Agent.json

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-18 20:05:01 +00:00