Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a82e305
wip: rework chat attachments
simcariou Jun 9, 2026
3676665
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 9, 2026
ba5d497
feat: Add drag n' drop in chat + xlsx, txt, image in fast ingest
simcariou Jun 9, 2026
ade8727
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 9, 2026
e9878a3
fix: make test
simcariou Jun 10, 2026
18bb42b
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 10, 2026
bc6cfbc
fix: remove "synthia" from pyproject.toml
simcariou Jun 10, 2026
09162de
fix: type-check
simcariou Jun 10, 2026
bae2af8
fix: kics on frontend Dockerfile
simcariou Jun 10, 2026
72418fc
fix: add transparency on drag and drop page
simcariou Jun 10, 2026
2033eed
impr: Overall design and use slices instead of hardcoded queries in f…
simcariou Jun 11, 2026
65106bf
impr: scrollable attachment list
simcariou Jun 11, 2026
62e5a62
wip: Add persisted attachments management
simcariou Jun 11, 2026
01d3543
impr: fast/delete and various fixes
simcariou Jun 12, 2026
d9a521a
impr: Deleting a conversation deletes all associated attachments
simcariou Jun 12, 2026
6e03295
impr: rework SearchConfig in chat
simcariou Jun 12, 2026
eb74424
impr: close SearchConfig when clicking away
simcariou Jun 12, 2026
bf4ae42
impr: Bind mcp chat options to managed chat
simcariou Jun 12, 2026
75cd0e4
fix: code-quality
simcariou Jun 12, 2026
401727f
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 12, 2026
feeb6be
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 12, 2026
3b0757b
fix: jsons schema for cp & test
simcariou Jun 12, 2026
26e5119
fix: kf json schema
simcariou Jun 12, 2026
3e205f9
fix: make test cp
simcariou Jun 12, 2026
22f9d96
fix: code-quality
simcariou Jun 12, 2026
e749865
fix: use the same library picker in the SearchConfig and in the agent…
simcariou Jun 12, 2026
c21056d
#1706 merged latest swift and improve migration strategy plans
Jun 13, 2026
fbe2019
impr: add translations and better chat ui
simcariou Jun 13, 2026
b77d2f6
#1706 setup specs for migration export import next features
Jun 15, 2026
a7cdcba
#1706 merged latest 1706
Jun 15, 2026
74e06e4
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 15, 2026
81fa534
fix: update helm schemas
simcariou Jun 15, 2026
112e630
impr: Allow document selection in library picker
simcariou Jun 15, 2026
f1fb72c
fix: Document scoping + deport library selection UI
simcariou Jun 15, 2026
651d524
feat: Allow mixing documents and librairies in vector search filtered…
simcariou Jun 15, 2026
488f9a5
fix: Better ui feedback
simcariou Jun 15, 2026
3a9029a
fix: code quality
simcariou Jun 15, 2026
a5f4727
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 15, 2026
a9216ec
fix: code-quality
simcariou Jun 15, 2026
d7b6155
impr: variable documentpicker height depending on the place in the chat
simcariou Jun 15, 2026
f965a38
Merge branch 'swift' into 1706-chat-04-chat-attachments-option-a-comp…
simcariou Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/control-plane-backend/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import control_plane_backend.models.agent_instance_models # noqa: F401
import control_plane_backend.models.prompt_models # noqa: F401
import control_plane_backend.models.purge_queue_models # noqa: F401
import control_plane_backend.models.session_attachment_models # noqa: F401
Comment thread
simcariou marked this conversation as resolved.
Dismissed
import control_plane_backend.models.session_metadata_models # noqa: F401
from alembic import context
from control_plane_backend.config.loader import load_configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""add session attachments

Revision ID: f2b3c4d5e6f7
Revises: be753abe25d7
Create Date: 2026-06-11 12:30:00.000000
"""

from __future__ import annotations

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision = "f2b3c4d5e6f7" # pragma: allowlist secret
down_revision = "b4c5d6e7f8a9" # pragma: allowlist secret
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"session_attachments",
sa.Column("session_id", sa.String(), nullable=False),
sa.Column("attachment_id", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("mime", sa.String(), nullable=True),
sa.Column("size_bytes", sa.Integer(), nullable=True),
sa.Column("summary_md", sa.Text(), nullable=False),
sa.Column("document_uid", sa.String(), nullable=True),
sa.Column("storage_key", sa.String(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("session_id", "attachment_id"),
)


def downgrade() -> None:
op.drop_table("session_attachments")
1 change: 1 addition & 0 deletions apps/control-plane-backend/config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ policies:

platform:
# Runtime pod references only. Managed agent instance enrollment is DB-backed.
knowledge_flow_base_url: http://127.0.0.1:8111/knowledge-flow/v1
runtime_catalog_sources:
- runtime_id: fred-samples-agents
base_url: http://127.0.0.1:8010/samples/agents/v1
Expand Down
1 change: 1 addition & 0 deletions apps/control-plane-backend/config/configuration_prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ policies:

platform:
# Runtime pod references only. Managed agent instance enrollment is DB-backed.
knowledge_flow_base_url: http://127.0.0.1:8111/knowledge-flow/v1
runtime_catalog_sources:
- runtime_id: fred-samples-agents
base_url: http://127.0.0.1:8010/samples/agents/v1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@
"frontend": {
"$ref": "#/$defs/FrontendBootstrapConfig"
},
"knowledge_flow_base_url": {
"default": "http://127.0.0.1:8111/knowledge-flow/v1",
"description": "Server-side base URL used by control-plane when it must orchestrate Knowledge Flow attachment cleanup on behalf of the authenticated user.",
"title": "Knowledge Flow Base Url",
"type": "string"
},
"runtime_catalog_sources": {
"items": {
"$ref": "#/$defs/RuntimeCatalogSourceConfig"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ConversationPolicyCatalog,
)
from control_plane_backend.scheduler.queue_store import PurgeQueueStore
from control_plane_backend.sessions.attachment_store import SessionAttachmentStore
from control_plane_backend.sessions.store import SessionMetadataStore

logger = logging.getLogger(__name__)
Expand All @@ -55,6 +56,7 @@ def __init__(self, configuration: Configuration):
self._rebac_engine: RebacEngine | None = None
self._agent_instance_store: AgentInstanceStore | None = None
self._session_metadata_store: SessionMetadataStore | None = None
self._session_attachment_store: SessionAttachmentStore | None = None
self._prompt_store: PromptStore | None = None
self._task_service: TaskService | None = None
self._evaluation_store: EvaluationStore | None = None
Expand Down Expand Up @@ -171,6 +173,13 @@ def get_session_metadata_store(self) -> SessionMetadataStore:
)
return self._session_metadata_store

def get_session_attachment_store(self) -> SessionAttachmentStore:
if self._session_attachment_store is None:
self._session_attachment_store = SessionAttachmentStore(
engine=self.get_pg_async_engine()
)
return self._session_attachment_store

def get_prompt_store(self) -> PromptStore:
if self._prompt_store is None:
self._prompt_store = PromptStore(engine=self.get_pg_async_engine())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ class PlatformConfig(BaseModel):
"""

frontend: FrontendBootstrapConfig = Field(default_factory=FrontendBootstrapConfig)
knowledge_flow_base_url: str = Field(
default="http://127.0.0.1:8111/knowledge-flow/v1",
description=(
"Server-side base URL used by control-plane when it must orchestrate "
"Knowledge Flow attachment cleanup on behalf of the authenticated user."
),
)
runtime_catalog_sources: list[RuntimeCatalogSourceConfig] = Field(
default_factory=list
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Kea→Swift platform import (MIGR-05).

See docs/swift/rfc/PLATFORM-IMPORT-RFC.md for the design.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Kea→Swift agent template mapping (MIGR-05).

Maps a kea-exported agent to its swift template, so the import can create the
equivalent managed `agent_instance`. See docs/swift/rfc/PLATFORM-IMPORT-RFC.md §7.

The mapping table is the single control point. Every exported agent is classified
into exactly one outcome:

- ``MAPPED`` — the kea template has a swift equivalent; create the agent_instance.
- ``IGNORED`` — a known kea built-in sample/demo; not user data, skipped on purpose.
- ``GAP`` — no mapping yet (or no resolvable template); the equivalent must be
built in fred-agents and added here. A real cutover requires zero gaps.

Scope is user-created agent instances; kea sample agents are intentionally not
migrated (swift provides its own catalog).
"""

from __future__ import annotations

import logging
from collections.abc import Mapping
from dataclasses import dataclass
from enum import Enum
from typing import Any

logger = logging.getLogger(__name__)


# Kea template identity → swift template id (``{source_runtime_id}:{source_agent_id}``).
# Keys are a v2 ``definition_ref`` or a legacy v1 ``class_path``. Swift ids are
# validated at import time against the live fred-agents ``/agents/templates`` catalog.
KEA_TO_SWIFT_TEMPLATE: dict[str, str] = {
# Agents users actually create on kea:
"v2.react.basic": "fred-agents:fred.github.assistant",
"v2.production.sql_analyst": "fred-agents:fred.github.sql_expert",
# Equivalents available if a real user instance uses them:
"agentic_backend.agents.v1.production.prometheus.prometheus_expert.Spot": "fred-agents:fred.github.sentinel",
"agentic_backend.agents.v1.production.rags.rag_expert.Rico": "fred-agents:fred.github.rag_expert",
"agentic_backend.agents.v1.production.tabular.tabular_expert.Tessa": "fred-agents:fred.github.sql_expert",
}

# Kea built-in sample/demo templates that are intentionally NOT migrated. Listed so
# preflight treats them as expected rather than as gaps requiring a new fred-agents
# template.
IGNORED_KEA_TEMPLATES: frozenset[str] = frozenset(
{
"v2.sample.bank_transfer",
"v2.deep.corpus_investigator",
"v2.production.dva_risk_validator.graph",
"v2.production.dva_risk_validator.qa",
}
)


class AgentMapOutcome(str, Enum):
"""How an exported kea agent is treated by the import."""

MAPPED = "mapped"
IGNORED = "ignored"
GAP = "gap"


@dataclass(frozen=True)
class AgentMapResult:
"""Result of classifying one exported kea agent.

``kea_template`` is the resolved template identity (``None`` when neither a
``definition_ref`` nor a ``class_path`` is present). ``swift_template_id`` is set
only when ``outcome`` is ``MAPPED``.
"""

outcome: AgentMapOutcome
kea_template: str | None
swift_template_id: str | None


def resolve_kea_template(payload: Mapping[str, Any]) -> str | None:
"""Return the kea template identity from an exported agent ``payload_json``.

v2 agents carry a top-level ``definition_ref``; legacy v1 agents carry a
top-level ``class_path``. ``definition_ref`` takes precedence. Returns ``None``
when neither is present (the agent is then a GAP).
"""
definition_ref = payload.get("definition_ref")
if isinstance(definition_ref, str) and definition_ref.strip():
return definition_ref
class_path = payload.get("class_path")
if isinstance(class_path, str) and class_path.strip():
return class_path
return None


def classify_agent(payload: Mapping[str, Any]) -> AgentMapResult:
"""Classify one exported kea agent ``payload_json`` into mapped / ignored / gap.

The agent is MAPPED when its template is in ``KEA_TO_SWIFT_TEMPLATE``, IGNORED
when it is a known sample, and a GAP otherwise (including when no template can be
resolved). Each GAP is a fred-agents template to build before cutover.
"""
kea_template = resolve_kea_template(payload)
if kea_template is None:
return AgentMapResult(AgentMapOutcome.GAP, None, None)
swift_template_id = KEA_TO_SWIFT_TEMPLATE.get(kea_template)
if swift_template_id is not None:
return AgentMapResult(AgentMapOutcome.MAPPED, kea_template, swift_template_id)
if kea_template in IGNORED_KEA_TEMPLATES:
return AgentMapResult(AgentMapOutcome.IGNORED, kea_template, None)
return AgentMapResult(AgentMapOutcome.GAP, kea_template, None)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from datetime import datetime

from sqlalchemy import DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column

from control_plane_backend.models.base import Base, utcnow


class SessionAttachmentRow(Base):
"""ORM model for the ``session_attachments`` table."""

__tablename__ = "session_attachments"

session_id: Mapped[str] = mapped_column(String, primary_key=True)
attachment_id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
mime: Mapped[str | None] = mapped_column(String, nullable=True)
size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
summary_md: Mapped[str] = mapped_column(Text, nullable=False)
document_uid: Mapped[str | None] = mapped_column(String, nullable=True)
storage_key: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=utcnow,
onupdate=utcnow,
)
Loading
Loading