Skip to content

Collection: making the module provider agnostic#508

Closed
nishika26 wants to merge 10 commits intomainfrom
enhancement/collection_provider_agnostic
Closed

Collection: making the module provider agnostic#508
nishika26 wants to merge 10 commits intomainfrom
enhancement/collection_provider_agnostic

Conversation

@nishika26
Copy link
Copy Markdown
Collaborator

@nishika26 nishika26 commented Dec 24, 2025

Summary

Target issue is #489

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

  • New Features

    • Provider-based collections: create/delete collections via pluggable LLM providers.
    • Collections can have a name and description; names are now validated for uniqueness.
    • New Evaluations APIs: upload/list/get/delete datasets and start/list/get evaluation runs.
  • Refactor

    • Collection workflows reworked to use provider abstractions (more extensible).
    • Evaluation service and dataset flows introduced to centralize evaluation functionality.
  • Tests

    • Updated test utilities and fixtures to exercise provider-based and evaluation flows.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

This PR introduces a provider-based collection provisioning system, expands collection schema (provider/name/description) with a DB migration, implements a provider registry and OpenAI provider, adds a full Evaluations feature (services, validators, routes, tests), refactors batch/job imports and polling, removes per-key API key helpers, and updates many tests and fixtures.

Changes

Cohort / File(s) Summary
Models & Migration
backend/app/models/collection.py, backend/app/models/__init__.py, backend/app/models/organization.py, backend/app/alembic/versions/041_extend_collection_table_for_provider_.py
Add ProviderType and provider, name, description to Collection; remove Organization.collections and organization_id from Collection; update request/option models; add DB migration to add provider/name/description and drop organization_id.
Provider System
backend/app/services/collections/providers/...
backend/app/services/collections/providers/__init__.py, .../base.py, .../openai.py, .../registry.py
New BaseProvider abstract class, OpenAIProvider implementation, and provider registry with get_llm_provider() factory and exports.
Collection Services & API
backend/app/services/collections/create_collection.py, .../delete_collection.py, .../helpers.py, backend/app/api/routes/collections.py, backend/app/crud/collection/collection.py
Replace legacy OpenAI-specific paths with provider-based create/delete/cleanup flows; add get_service_name() and ensure_unique_name(); call ensure_unique_name() in route; add CollectionCrud.exists_by_name().
Batch / Job Refactor
backend/app/core/batch/__init__.py, .../operations.py, .../polling.py, backend/app/crud/job/*, backend/app/crud/job/batch.py
Re-export batch functions from new core locations, remove inline poll in operations, add poll_batch_status module, add crud.job re-export layer and job/batch re-exports.
Evaluations Feature
backend/app/api/routes/evaluations/*, backend/app/services/evaluations/*, backend/app/services/evaluations/validators.py, backend/app/crud/evaluations/*
Remove old evaluation route file; add new evaluations package (routes: dataset, evaluation), services (dataset/evaluation), validators, CRUD adjustments, and associated orchestration (Langfuse/object store integration).
Security
backend/app/core/security.py, backend/app/tests/core/test_security.py
Remove per-key encrypt_api_key / decrypt_api_key helpers and corresponding tests.
Tests & Utilities
many files under backend/app/tests/..., backend/app/tests/utils/llm_provider.py, backend/app/tests/utils/collection.py, backend/app/tests/utils/document.py
Widespread test updates to use provider mocks (get_mock_provider), rename helpers (get_collectionget_assistant_collection, add get_vector_store_collection), move/remove fixtures (crawler), update import paths, and add many new evaluation tests.
Misc & CI
backend/app/api/main.py, .github/workflows/*, backend/app/core/batch/operations.py
Switch main routing import to new evaluations module; change GitHub action upload-artifact version; add if: false to staging CD job.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CreateService as create_collection.py
    participant Registry as get_llm_provider
    participant Provider as OpenAIProvider
    participant DocumentCrud
    participant Storage
    participant Database

    Client->>CreateService: execute_job(request, with_assistant, ...)
    CreateService->>Registry: get_llm_provider(provider)
    Registry-->>CreateService: provider instance
    CreateService->>Provider: create(collection_request, Storage, DocumentCrud)
    Provider->>DocumentCrud: batch_documents / read each
    Provider->>Storage: upload files (if used)
    Provider->>Provider: create vector store / assistant
    Provider-->>CreateService: Collection result (llm_service_id, llm_service_name)
    CreateService->>Database: persist Collection with provider metadata
    Database-->>CreateService: saved
    CreateService-->>Client: callback success/failure
Loading
sequenceDiagram
    participant Client
    participant DeleteService as delete_collection.py
    participant Database
    participant Registry as get_llm_provider
    participant Provider as OpenAIProvider

    Client->>DeleteService: start_job(collection_id,...)
    DeleteService->>Database: fetch Collection
    Database-->>DeleteService: Collection(provider, llm_service_id, llm_service_name)
    DeleteService->>Registry: get_llm_provider(collection.provider)
    Registry-->>DeleteService: provider instance
    DeleteService->>Provider: delete(collection)
    Provider-->>DeleteService: provider deletion result
    DeleteService->>Database: delete local Collection record
    DeleteService-->>Client: callback success/failure
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • avirajsingh7
  • Prajna1999

Poem

🐰 I hopped through models, tests, and code,
A provider path where old flows strode,
OpenAI tucked inside a class so neat,
Jobs create and delete with tidy feet,
Cheers — a rabbit’s dance for CI green light! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Collection: making the module provider agnostic' is clear and directly summarizes the main objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nishika26 nishika26 self-assigned this Dec 24, 2025
@nishika26 nishika26 added the enhancement New feature or request label Dec 24, 2025
@nishika26 nishika26 linked an issue Dec 24, 2025 that may be closed by this pull request
@nishika26 nishika26 marked this pull request as ready for review December 26, 2025 04:23
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/collections/create_collection.py (1)

269-270: Potential NameError if CreationRequest parsing fails.

If CreationRequest(**request) on line 156 raises an exception, creation_request is never assigned. The check on line 269 will then raise a NameError.

Proposed fix: initialize creation_request before try block or guard the check
+    creation_request = None
+
     try:
         creation_request = CreationRequest(**request)
         # ...

     except Exception as err:
         # ...

-        if creation_request and creation_request.callback_url and collection_job:
+        if creation_request is not None and creation_request.callback_url and collection_job:
             failure_payload = build_failure_payload(collection_job, str(err))
             send_callback(creation_request.callback_url, failure_payload)
🧹 Nitpick comments (11)
backend/app/services/collections/helpers.py (1)

17-25: Consider raising an error or logging for unknown providers.

Returning an empty string for unknown providers could lead to silent failures downstream. Consider logging a warning or raising a ValueError for unsupported providers to make debugging easier.

🔎 Suggested improvement
 def get_service_name(provider: str) -> str:
     """Get the collection service name for a provider."""
     names = {
         "openai": "openai vector store",
         #   "bedrock": "bedrock knowledge base",
         #  "gemini": "gemini file search store",
     }
-    return names.get(provider.lower(), "")
+    service_name = names.get(provider.lower())
+    if service_name is None:
+        logger.warning(f"[get_service_name] Unknown provider: {provider}")
+        return ""
+    return service_name
backend/app/services/collections/providers/base.py (3)

30-53: Docstring parameters don't match the method signature.

The docstring mentions batch_size, with_assistant, and assistant_options parameters that don't exist in the actual method signature. Also:

  • Line 48: "CreateCollectionresult" → "CreateCollectionResult" (typo)
  • Line 51: "kb_blob" → "collection_blob" (field name mismatch)
  • Line 53: error message says "execute method" but method is named "create"
Proposed fix
     @abstractmethod
     def create(
         self,
         collection_request: CreationRequest,
         storage: CloudStorage,
         document_crud: DocumentCrud,
     ) -> CreateCollectionResult:
         """Create collection with documents and optionally an assistant.

         Args:
-            collection_params: Collection parameters (name, description, chunking_params, etc.)
+            collection_request: Creation request containing collection params and options
             storage: Cloud storage instance for file access
             document_crud: DocumentCrud instance for fetching documents
-            batch_size: Number of documents to process per batch
-            with_assistant: Whether to create an assistant/agent
-            assistant_options: Options for assistant creation (provider-specific)

         Returns:
-            CreateCollectionresult containing:
+            CreateCollectionResult containing:
             - llm_service_id: ID of the created resource (vector store or assistant)
             - llm_service_name: Name of the service
-            - kb_blob: All collection params except documents
+            - collection_blob: All collection params except documents
         """
-        raise NotImplementedError("Providers must implement execute method")
+        raise NotImplementedError("Providers must implement create method")

55-65: Docstring Args don't match the method signature.

The docstring mentions llm_service_id and llm_service_name as parameters, but the actual signature only accepts collection: Collection.

Proposed fix
     @abstractmethod
     def delete(self, collection: Collection) -> None:
         """Delete remote resources associated with a collection.

         Called when a collection is being deleted and remote resources need to be cleaned up.

         Args:
-            llm_service_id: ID of the resource to delete
-            llm_service_name: Name of the service (determines resource type)
+            collection: The collection whose remote resources should be deleted
         """
         raise NotImplementedError("Providers must implement delete method")

67-76: Typo in docstring.

Line 74: "CreateCollectionresult" should be "CreateCollectionResult".

Proposed fix
-            collection_result: The CreateCollectionresult returned from execute, containing resource IDs
+            collection_result: The CreateCollectionResult returned from create, containing resource IDs
backend/app/services/collections/create_collection.py (1)

35-42: Unused with_assistant parameter.

The with_assistant parameter is accepted but never used in start_job. The assistant creation logic is now determined by checking model and instructions in the provider. Consider removing this parameter if it's no longer needed.

Proposed fix
 def start_job(
     db: Session,
     request: CreationRequest,
     project_id: int,
     collection_job_id: UUID,
-    with_assistant: bool,
     organization_id: int,
 ) -> str:
backend/app/services/collections/providers/openai.py (4)

2-2: Unused import: Any.

The Any type is imported but not used in this file.

Proposed fix
 import logging
-from typing import Any
 
 from openai import OpenAI

24-26: Redundant self.client assignment.

super().__init__(client) already assigns self.client = client in BaseProvider.__init__. The second assignment on line 26 is redundant.

Proposed fix
     def __init__(self, client: OpenAI):
         super().__init__(client)
-        self.client = client

62-65: Log messages reference wrong method name.

The log prefix says [OpenAIProvider.execute] but the method is named create. Per coding guidelines, log messages should be prefixed with the function name.

Proposed fix for all occurrences in create method
             logger.info(
-                "[OpenAIProvider.execute] Vector store created | "
+                "[OpenAIProvider.create] Vector store created | "
                 f"vector_store_id={vector_store.id}, batches={len(docs_batches)}"
             )

Apply similar changes to lines 93-95, 104-105, and 114-118.


60-60: Consider explicit loop for generator consumption.

Using list() to consume a generator whose result is discarded can be unclear. A for loop or collections.deque(maxlen=0) pattern would make intent clearer.

Proposed alternative
-            list(vector_store_crud.update(vector_store.id, storage, docs_batches))
+            for _ in vector_store_crud.update(vector_store.id, storage, docs_batches):
+                pass
backend/app/services/collections/providers/registry.py (1)

61-69: Unreachable else branch and logging format.

The else branch (lines 65-69) is unreachable because LLMProvider.get(provider) on line 47 already raises ValueError for unsupported providers. Also, the log message on line 67 should use square brackets per coding guidelines: [get_llm_provider].

Proposed fix: remove unreachable code or convert to assertion
     if provider == LLMProvider.OPENAI:
         if "api_key" not in credentials:
             raise ValueError("OpenAI credentials not configured for this project.")
         client = OpenAI(api_key=credentials["api_key"])
-    else:
-        logger.error(
-            f"[get_llm_provider] Unsupported provider type requested: {provider}"
-        )
-        raise ValueError(f"Provider '{provider}' is not supported.")
+    else:
+        # This branch is unreachable as LLMProvider.get validates the provider,
+        # but kept as defensive programming for future provider additions.
+        raise AssertionError(f"Unhandled provider: {provider}")

     return provider_class(client=client)
backend/app/models/collection/response.py (1)

20-29: Add provider field to CollectionPublic.

The Collection database model includes a provider field (ProviderType enum) that represents the LLM provider (e.g., "openai"). This field is missing from CollectionPublic and should be exposed in the response schema. Per learnings, provider and llm_service_name serve different purposes—provider indicates the LLM provider name while llm_service_name specifies the particular service from that provider. Exposing both fields provides complete information to API consumers about the collection's LLM configuration.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91941f9 and 946e7c7.

📒 Files selected for processing (15)
  • backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py
  • backend/app/models/__init__.py
  • backend/app/models/collection/__init__.py
  • backend/app/models/collection/request.py
  • backend/app/models/collection/response.py
  • backend/app/services/collections/create_collection.py
  • backend/app/services/collections/delete_collection.py
  • backend/app/services/collections/helpers.py
  • backend/app/services/collections/providers/__init__.py
  • backend/app/services/collections/providers/base.py
  • backend/app/services/collections/providers/openai.py
  • backend/app/services/collections/providers/registry.py
  • backend/app/tests/api/routes/collections/test_collection_info.py
  • backend/app/tests/api/routes/collections/test_collection_list.py
  • backend/app/tests/utils/collection.py
🧰 Additional context used
📓 Path-based instructions (6)
backend/app/services/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Implement business logic in services located in backend/app/services/

Files:

  • backend/app/services/collections/delete_collection.py
  • backend/app/services/collections/providers/openai.py
  • backend/app/services/collections/providers/base.py
  • backend/app/services/collections/providers/registry.py
  • backend/app/services/collections/create_collection.py
  • backend/app/services/collections/providers/__init__.py
  • backend/app/services/collections/helpers.py
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Always add type hints to all function parameters and return values in Python code
Prefix all log messages with the function name in square brackets: logger.info(f"[function_name] Message {mask_string(sensitive_value)}")
Use Python 3.11+ with type hints throughout the codebase

Files:

  • backend/app/services/collections/delete_collection.py
  • backend/app/services/collections/providers/openai.py
  • backend/app/services/collections/providers/base.py
  • backend/app/services/collections/providers/registry.py
  • backend/app/tests/utils/collection.py
  • backend/app/tests/api/routes/collections/test_collection_list.py
  • backend/app/services/collections/create_collection.py
  • backend/app/services/collections/providers/__init__.py
  • backend/app/services/collections/helpers.py
  • backend/app/models/collection/__init__.py
  • backend/app/models/collection/response.py
  • backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py
  • backend/app/tests/api/routes/collections/test_collection_info.py
  • backend/app/models/__init__.py
  • backend/app/models/collection/request.py
backend/app/tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use factory pattern for test fixtures in backend/app/tests/

Files:

  • backend/app/tests/utils/collection.py
  • backend/app/tests/api/routes/collections/test_collection_list.py
  • backend/app/tests/api/routes/collections/test_collection_info.py
backend/app/models/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use sa_column_kwargs["comment"] to describe database columns, especially for non-obvious purposes, status/type fields, JSON/metadata columns, and foreign keys

Files:

  • backend/app/models/collection/__init__.py
  • backend/app/models/collection/response.py
  • backend/app/models/__init__.py
  • backend/app/models/collection/request.py
backend/app/alembic/versions/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Generate database migrations using alembic revision --autogenerate -m "Description" --rev-id <number> where rev-id is the latest existing revision ID + 1

Files:

  • backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py
backend/app/models/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use SQLModel for database models located in backend/app/models/

Files:

  • backend/app/models/__init__.py
🧠 Learnings (5)
📓 Common learnings
Learnt from: nishika26
Repo: ProjectTech4DevAI/kaapi-backend PR: 502
File: backend/app/models/collection.py:29-32
Timestamp: 2025-12-17T10:16:25.880Z
Learning: In backend/app/models/collection.py, the `provider` field indicates the LLM provider name (e.g., "openai"), while `llm_service_name` specifies which particular service from that provider is being used. These fields serve different purposes and are not redundant.
📚 Learning: 2025-12-17T10:16:25.880Z
Learnt from: nishika26
Repo: ProjectTech4DevAI/kaapi-backend PR: 502
File: backend/app/models/collection.py:29-32
Timestamp: 2025-12-17T10:16:25.880Z
Learning: In backend/app/models/collection.py, the `provider` field indicates the LLM provider name (e.g., "openai"), while `llm_service_name` specifies which particular service from that provider is being used. These fields serve different purposes and are not redundant.

Applied to files:

  • backend/app/services/collections/delete_collection.py
  • backend/app/services/collections/providers/openai.py
  • backend/app/services/collections/providers/registry.py
  • backend/app/tests/utils/collection.py
  • backend/app/tests/api/routes/collections/test_collection_list.py
  • backend/app/services/collections/providers/__init__.py
  • backend/app/services/collections/helpers.py
  • backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py
  • backend/app/tests/api/routes/collections/test_collection_info.py
📚 Learning: 2025-12-17T15:39:30.469Z
Learnt from: CR
Repo: ProjectTech4DevAI/kaapi-backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T15:39:30.469Z
Learning: Applies to backend/app/crud/*.py : Use CRUD pattern for database access operations located in `backend/app/crud/`

Applied to files:

  • backend/app/services/collections/delete_collection.py
📚 Learning: 2025-12-17T10:16:16.173Z
Learnt from: nishika26
Repo: ProjectTech4DevAI/kaapi-backend PR: 502
File: backend/app/models/collection.py:29-32
Timestamp: 2025-12-17T10:16:16.173Z
Learning: In backend/app/models/collection.py, treat provider as the LLM provider name (e.g., 'openai') and llm_service_name as the specific service from that provider. These fields serve different purposes and should remain non-redundant. Document their meanings, add clear type hints (e.g., provider: str, llm_service_name: str), and consider a small unit test or validation to ensure they are distinct and used appropriately, preventing accidental aliasing or duplication across the model or serializers.

Applied to files:

  • backend/app/models/collection/__init__.py
  • backend/app/models/collection/response.py
  • backend/app/models/__init__.py
  • backend/app/models/collection/request.py
📚 Learning: 2025-12-17T15:39:30.469Z
Learnt from: CR
Repo: ProjectTech4DevAI/kaapi-backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T15:39:30.469Z
Learning: Applies to backend/app/models/**/*.py : Use `sa_column_kwargs["comment"]` to describe database columns, especially for non-obvious purposes, status/type fields, JSON/metadata columns, and foreign keys

Applied to files:

  • backend/app/models/collection/request.py
🧬 Code graph analysis (11)
backend/app/services/collections/delete_collection.py (4)
backend/app/services/collections/providers/registry.py (1)
  • get_llm_provider (44-71)
backend/app/services/collections/providers/openai.py (1)
  • delete (121-148)
backend/app/services/collections/providers/base.py (1)
  • delete (56-65)
backend/app/crud/collection/collection.py (1)
  • delete (103-111)
backend/app/services/collections/providers/base.py (5)
backend/app/crud/document/document.py (1)
  • DocumentCrud (13-134)
backend/app/core/cloud/storage.py (1)
  • CloudStorage (113-141)
backend/app/models/collection/request.py (2)
  • CreationRequest (224-236)
  • Collection (26-92)
backend/app/models/collection/response.py (1)
  • CreateCollectionResult (10-13)
backend/app/services/collections/providers/openai.py (3)
  • create (28-119)
  • delete (121-148)
  • cleanup (150-160)
backend/app/tests/utils/collection.py (2)
backend/app/models/collection/request.py (1)
  • ProviderType (16-19)
backend/app/services/collections/helpers.py (1)
  • get_service_name (18-25)
backend/app/tests/api/routes/collections/test_collection_list.py (1)
backend/app/services/collections/helpers.py (1)
  • get_service_name (18-25)
backend/app/services/collections/providers/__init__.py (3)
backend/app/services/collections/providers/base.py (1)
  • BaseProvider (9-84)
backend/app/services/collections/providers/openai.py (1)
  • OpenAIProvider (21-160)
backend/app/services/collections/providers/registry.py (2)
  • LLMProvider (14-41)
  • get_llm_provider (44-71)
backend/app/services/collections/helpers.py (1)
backend/app/services/collections/providers/registry.py (1)
  • get (28-36)
backend/app/models/collection/__init__.py (2)
backend/app/models/collection/request.py (7)
  • Collection (26-92)
  • CreationRequest (224-236)
  • DeletionRequest (239-243)
  • CallbackRequest (197-203)
  • AssistantOptions (141-194)
  • CreateCollectionParams (106-138)
  • ProviderType (16-19)
backend/app/models/collection/response.py (4)
  • CollectionIDPublic (16-17)
  • CollectionPublic (20-29)
  • CollectionWithDocsPublic (32-33)
  • CreateCollectionResult (10-13)
backend/app/models/collection/response.py (1)
backend/app/models/document.py (1)
  • DocumentPublic (72-85)
backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py (2)
backend/app/services/collections/providers/openai.py (1)
  • create (28-119)
backend/app/services/collections/providers/base.py (1)
  • create (31-53)
backend/app/tests/api/routes/collections/test_collection_info.py (1)
backend/app/services/collections/helpers.py (1)
  • get_service_name (18-25)
backend/app/models/collection/request.py (3)
backend/app/core/util.py (1)
  • now (11-12)
backend/app/models/organization.py (1)
  • Organization (44-82)
backend/app/models/project.py (1)
  • Project (51-107)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.12, 6)
🔇 Additional comments (25)
backend/app/services/collections/helpers.py (1)

108-111: LGTM!

The refactor to use get_service_name("openai") instead of the removed constant is correct and maintains the existing behavior while aligning with the new provider abstraction.

backend/app/models/collection/request.py (4)

130-138: LGTM!

The deduplication logic in model_post_init correctly removes duplicate documents by ID while preserving order.


214-221: LGTM!

The normalize_provider validator correctly handles case-insensitive provider matching by normalizing to lowercase before validation.


224-243: LGTM!

The CreationRequest and DeletionRequest models are well-structured, properly compose their parent classes, and provide clear field descriptions.


35-47: ENUM type name is consistent. The provider field correctly uses name="providertype" (lowercase) in both the model definition and the Alembic migration at 041_adding_blob_column_in_collection_table.py. The create_type difference is intentional—the migration uses create_type=True to create the type, while the model uses create_type=False to use the existing type.

backend/app/tests/api/routes/collections/test_collection_list.py (2)

10-10: LGTM!

Importing get_service_name from the helpers module is correct and aligns with the provider abstraction changes.


105-106: LGTM!

Using get_service_name("openai") instead of a hardcoded string improves maintainability and ensures consistency with the service layer.

backend/app/tests/api/routes/collections/test_collection_info.py (2)

12-12: LGTM!

Import aligns with the provider abstraction pattern used across test files.


167-168: LGTM!

Assertion updated consistently with other test files to use the helper function.

backend/app/tests/utils/collection.py (3)

11-14: LGTM!

Imports correctly added to support the provider abstraction in test utilities.


42-50: LGTM!

The get_collection function correctly sets provider=ProviderType.OPENAI on the created Collection, aligning with the new provider-based model.


67-75: LGTM!

The get_vector_store_collection function correctly uses both get_service_name("openai") for the service name and ProviderType.OPENAI for the provider field, maintaining consistency with the provider abstraction.

backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py (1)

24-54: LGTM on the safe migration pattern.

The upgrade correctly follows the safe pattern for adding a NOT NULL column with existing data:

  1. Add column as nullable
  2. Backfill existing rows with default value
  3. Alter column to NOT NULL
backend/app/services/collections/delete_collection.py (2)

17-20: LGTM!

Imports correctly updated to use the new provider registry pattern, removing direct OpenAI CRUD dependencies.


159-180: Session management looks correct.

The provider is obtained within a session context (for credential lookup), but provider.delete(collection) is called outside the session block. This is appropriate since the external API call shouldn't hold the database session open.

backend/app/models/__init__.py (1)

9-19: LGTM!

The expanded exports correctly expose the new collection-related types (CreateCollectionParams, CreateCollectionResult, CreationRequest, DeletionRequest, ProviderType) needed for the provider-agnostic collection management.

backend/app/services/collections/providers/base.py (1)

78-84: LGTM!

The get_provider_name utility cleanly derives a lowercase provider name from the class name by convention.

backend/app/services/collections/providers/__init__.py (1)

1-6: LGTM!

The re-exports consolidate the provider package's public API cleanly. Consider adding an __all__ list for explicit export control, though this is optional.

backend/app/models/collection/response.py (2)

10-17: LGTM!

CreateCollectionResult and CollectionIDPublic models are well-defined with proper type hints.


32-33: LGTM!

CollectionWithDocsPublic correctly extends CollectionPublic with an optional documents list.

backend/app/services/collections/create_collection.py (1)

254-260: LGTM!

The provider cleanup is properly guarded—only attempts cleanup if both provider and result are available, with error handling to prevent masking the original exception.

backend/app/models/collection/__init__.py (1)

1-15: LGTM!

The package correctly aggregates and re-exports public types from the request and response submodules, providing a clean import surface.

backend/app/services/collections/providers/openai.py (1)

121-160: LGTM!

The delete and cleanup methods correctly handle both assistant and vector store resources with proper error handling and logging.

backend/app/services/collections/providers/registry.py (2)

14-41: LGTM!

The LLMProvider registry pattern is well-structured and extensible, with clear methods for provider lookup and listing supported providers.


44-59: LGTM!

The factory function properly validates credentials existence and provider-specific requirements before constructing the client.

Comment on lines +66 to +76
def downgrade():
op.alter_column(
"collection",
"llm_service_name",
existing_type=sa.VARCHAR(),
comment="Name of the LLM service provider",
existing_comment="Name of the LLM service",
existing_nullable=False,
)
op.drop_column("collection", "provider")
op.drop_column("collection", "collection_blob")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing ENUM type drop in downgrade.

The downgrade() function drops the provider and collection_blob columns but doesn't drop the providertype ENUM type. This could leave orphaned types in the database after a rollback.

🔎 Proposed fix
 def downgrade():
     op.alter_column(
         "collection",
         "llm_service_name",
         existing_type=sa.VARCHAR(),
         comment="Name of the LLM service provider",
         existing_comment="Name of the LLM service",
         existing_nullable=False,
     )
     op.drop_column("collection", "provider")
     op.drop_column("collection", "collection_blob")
+    provider_enum.drop(op.get_bind(), checkifexists=True)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/alembic/versions/041_adding_blob_column_in_collection_table.py
around lines 66 to 76, the downgrade drops the provider and collection_blob
columns but does not remove the providertype ENUM type, leaving an orphaned type
in the database; update downgrade to drop the providertype ENUM after dropping
the provider column by using op.execute or sa.Enum(...).drop(op.get_bind(),
checkfirst=True) (or op.execute('DROP TYPE IF EXISTS providertype') depending on
DB) to remove the ENUM type safely and ensure checkfirst behavior so downgrade
is idempotent.

Comment on lines +95 to +103
class DocumentInput(SQLModel):
"""Document to be added to knowledge base."""

name: str | None = Field(
description="Display name for the document",
)
batch_size: int = Field(
default=1,
description=(
"Number of documents to send to OpenAI in a single "
"transaction. See the `file_ids` parameter in the "
"vector store [create batch](https://platform.openai.com/docs/api-reference/vector-stores-file-batches/createBatch)."
),
id: UUID = Field(
description="Reference to uploaded file/document in Kaapi",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

DocumentInput.name field missing default value.

The name field is typed as str | None but lacks a default=None, which will make it a required field despite being optional by type.

🔎 Proposed fix
 class DocumentInput(SQLModel):
     """Document to be added to knowledge base."""
 
     name: str | None = Field(
+        default=None,
         description="Display name for the document",
     )
     id: UUID = Field(
         description="Reference to uploaded file/document in Kaapi",
     )
🤖 Prompt for AI Agents
In backend/app/models/collection/request.py around lines 95 to 103, the
DocumentInput.name is annotated as optional (str | None) but lacks a default so
Pydantic/SQLModel treats it as required; update the Field declaration to include
default=None (i.e., name: str | None = Field(default=None, description="Display
name for the document")) so the field is truly optional and behaves as intended.

Comment on lines +109 to +112
name: str | None = Field(
min_length=1,
description="Name of the knowledge base to create or update",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the file to see the actual context
fd backend/app/models/collection/request.py --type f

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

# Read the specific file around lines 109-112
if [ -f "backend/app/models/collection/request.py" ]; then
  wc -l backend/app/models/collection/request.py
fi

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 119


🏁 Script executed:

# Get the file structure to understand imports and class definition
head -50 backend/app/models/collection/request.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1386


🏁 Script executed:

# Get lines 100-120 to see the name field and context
sed -n '100,120p' backend/app/models/collection/request.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 729


🏁 Script executed:

python3 << 'EOF'
from pydantic import Field
from sqlmodel import SQLModel

class CreateCollectionParams(SQLModel):
    name: str | None = Field(
        min_length=1,
        description="Name of the knowledge base to create or update",
    )

# Test 1: None value
try:
    t1 = CreateCollectionParams(name=None)
    print(f'Test 1 - None accepted: {t1.name}')
except Exception as e:
    print(f'Test 1 - Validation error with None: {type(e).__name__}: {e}')

# Test 2: Empty string
try:
    t2 = CreateCollectionParams(name="")
    print(f'Test 2 - Empty string accepted: {t2.name}')
except Exception as e:
    print(f'Test 2 - Validation error with empty string: {type(e).__name__}: {e}')

# Test 3: Valid string
try:
    t3 = CreateCollectionParams(name="test")
    print(f'Test 3 - Valid string accepted: {t3.name}')
except Exception as e:
    print(f'Test 3 - Validation error with valid string: {type(e).__name__}: {e}')

# Test 4: No name provided
try:
    t4 = CreateCollectionParams()
    print(f'Test 4 - No name provided, defaults to: {t4.name}')
except Exception as e:
    print(f'Test 4 - Validation error when name not provided: {type(e).__name__}: {e}')
EOF

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 161


🏁 Script executed:

# Check the full CreateCollectionParams class and surrounding context
sed -n '105,145p' backend/app/models/collection/request.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1494


🏁 Script executed:

# Check if there are imports or other usage patterns in the file
grep -n "sa_column_kwargs" backend/app/models/collection/request.py | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 659


Add explicit default=None to the name field.

The name field is typed as str | None but lacks an explicit default=None, while other Optional fields in this class (description, chunking_params, additional_params) all include it. This inconsistency can cause unexpected validation behavior in Pydantic. Add default=None to match the pattern: name: str | None = Field(min_length=1, default=None, description="...").

🤖 Prompt for AI Agents
In backend/app/models/collection/request.py around lines 109 to 112, the name
field is annotated as str | None but lacks an explicit default=None whereas
other optional fields include it; update the Field call to add default=None
(i.e., Field(min_length=1, default=None, description="Name of the knowledge base
to create or update")) so Pydantic treats it consistently and avoids unexpected
validation behavior.

Comment on lines +180 to +184
result = provider.create(
collection_request=creation_request,
storage=storage,
document_crud=document_crud,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

provider.create called outside session context—verify document_crud remains usable.

document_crud is created inside a with Session(engine) block (lines 159-178), but provider.create is called after the block exits. Since DocumentCrud holds a reference to the session, operations like read_each inside batch_documents may fail if the session is closed.

Proposed fix: move provider.create inside the session block
         with Session(engine) as session:
             collection_job_crud = CollectionJobCrud(session, project_id)
             collection_job = collection_job_crud.read_one(job_uuid)
             collection_job = collection_job_crud.update(
                 job_uuid,
                 CollectionJobUpdate(
                     task_id=task_id,
                     status=CollectionJobStatus.PROCESSING,
                 ),
             )

             storage = get_cloud_storage(session=session, project_id=project_id)
             document_crud = DocumentCrud(session, project_id)

             provider = get_llm_provider(
                 session=session,
                 provider=creation_request.provider,
                 project_id=project_id,
                 organization_id=organization_id,
             )

-        result = provider.create(
-            collection_request=creation_request,
-            storage=storage,
-            document_crud=document_crud,
-        )
+            result = provider.create(
+                collection_request=creation_request,
+                storage=storage,
+                document_crud=document_crud,
+            )
🤖 Prompt for AI Agents
In backend/app/services/collections/create_collection.py around lines 180 to
184, provider.create is being invoked after the with Session(engine) block that
constructed document_crud has closed, which can leave DocumentCrud holding a
closed session; move the provider.create(...) call (and any dependent logic like
batch_documents or document reads) inside the with Session(engine): block where
document_crud is created so the CRUD instance uses an open session, or
alternatively refactor DocumentCrud so it does not capture the session (e.g.,
pass sessions per-call) if keeping the call outside is required.

Comment on lines +173 to +180
provider = get_llm_provider(
session=session,
provider=deletion_request.provider,
project_id=project_id,
organization_id=organization_id,
)

# Delete the corresponding OpenAI resource (vector store or assistant)
if is_vector:
OpenAIVectorStoreCrud(client).delete(llm_service_id)
else:
OpenAIAssistantCrud(client).delete(llm_service_id)
provider.delete(collection)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and content around the problematic lines
head -n 200 backend/app/services/collections/delete_collection.py | tail -n +150

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1840


🏁 Script executed:

# Also get the full file to understand the context
wc -l backend/app/services/collections/delete_collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 132


🏁 Script executed:

# Search for provider validation patterns in this file and related files
rg "collection\.provider" backend/app/services/collections/delete_collection.py -A3 -B3

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

# Search for deletion_request and how it's used
rg "deletion_request" backend/app/services/collections/delete_collection.py -A2 -B2

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 952


🏁 Script executed:

# Get the full delete_collection.py file content
cat -n backend/app/services/collections/delete_collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 8238


🏁 Script executed:

# Find the Collection and DeletionRequest models
fd -e py | xargs rg "class Collection\b|class DeletionRequest" -A10

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1228


🏁 Script executed:

# Check the get_llm_provider function to see if it validates provider
rg "def get_llm_provider" -A20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3605


Add provider mismatch validation to prevent deletion with wrong provider.

The code uses deletion_request.provider to instantiate the LLM provider, but collection has its own provider attribute (defined in the Collection model). Without validating these match, deletion could attempt to use the wrong provider's client to delete a collection.

Add a validation check after retrieving the collection:

if deletion_request.provider != collection.provider:
    raise ValueError(f"Provider mismatch: request={deletion_request.provider}, collection={collection.provider}")
🤖 Prompt for AI Agents
In backend/app/services/collections/delete_collection.py around lines 173 to
180, after you retrieve the collection and before calling
get_llm_provider()/provider.delete(), validate that deletion_request.provider
matches collection.provider and raise a ValueError when they differ; add a
conditional that compares deletion_request.provider and collection.provider and
raises a ValueError with a clear message like "Provider mismatch: request=<...>,
collection=<...>" so provider.delete is only invoked when the providers match.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
backend/app/tests/crud/collections/collection/test_crud_collection_read_all.py (1)

14-28: Same issues: double creation, unused variable, missing type hint.

This function has the same problems as mk_collection in test_crud_collection_read_one.py:

  1. get_assistant_collection already persists the collection, then crud.create(collection, documents) at line 26 attempts to create it again.
  2. The client variable on line 19 is unused.
  3. Missing return type hint for create_collections.
Proposed fix
-def create_collections(db: Session, n: int):
+def create_collections(db: Session, n: int) -> int:
     crud = None
     project = get_project(db)
     openai_mock = OpenAIMock()
     with openai_mock.router:
-        client = OpenAI(api_key="sk-test-key")
+        _ = OpenAI(api_key="sk-test-key")  # Required for mock initialization
         for _ in range(n):
             collection = get_assistant_collection(db, project=project)
             store = DocumentStore(db, project_id=collection.project_id)
             documents = store.fill(1)
             if crud is None:
                 crud = CollectionCrud(db, collection.project_id)
-            crud.create(collection, documents)
+            # Collection already created; associate documents separately if needed

         return crud.project_id
backend/app/tests/utils/collection.py (1)

53-75: Bug: get_service_name ignores the provider parameter.

Line 71 calls get_service_name("openai") with a hardcoded string, ignoring the provider parameter passed to this function. This means vector store collections for non-OpenAI providers would incorrectly use "openai vector store" as the service name.

Additionally:

  • Line 73 uses provider.upper() (string), but should use ProviderType enum for type safety and consistency with get_assistant_collection.
  • Missing type hint for project parameter.
Proposed fix
+from app.models import Project
+
 def get_vector_store_collection(
     db: Session,
-    project,
+    project: Project,
     *,
     vector_store_id: Optional[str] = None,
     collection_id: Optional[UUID] = None,
-    provider: str,
+    provider: ProviderType,
 ) -> Collection:
     """
     Create a Collection configured for the Vector Store path.
     execute_job will treat this as `is_vector = True` and use vector store id.
     """
     if vector_store_id is None:
         vector_store_id = f"vs_{uuid4().hex}"

     collection = Collection(
         id=collection_id or uuid4(),
         project_id=project.id,
-        llm_service_name=get_service_name("openai"),
+        llm_service_name=get_service_name(provider.value.lower()),
         llm_service_id=vector_store_id,
-        provider=provider.upper(),
+        provider=provider,
     )
     return CollectionCrud(db, project.id).create(collection)
backend/app/tests/crud/collections/collection/test_crud_collection_read_one.py (1)

14-23: Add return type hint to test fixture.

The function is missing a return type hint. Add -> Collection to the function signature per coding guidelines.

The client variable is necessary for OpenAI mock router initialization within the with openai_mock.router: context and should be kept. The double create() call works as intended—the second call triggers IntegrityError handling which returns the existing collection, then documents are associated via DocumentCollectionCrud.

backend/app/tests/crud/collections/collection/test_crud_collection_delete.py (1)

15-34: Add full type hints and prefer ProviderType enum.

Keeps tests aligned with typing rules and avoids string-literal drift.

✍️ Suggested update
+from typing import Optional
@@
-from app.models import APIKey, Collection
+from app.models import APIKey, Collection, ProviderType
@@
-def get_assistant_collection_for_delete(
-    db: Session, client=None, project_id: int = None
-) -> Collection:
+def get_assistant_collection_for_delete(
+    db: Session, client: OpenAI | None = None, project_id: int | None = None
+) -> Collection:
@@
-        provider="OPENAI",
+        provider=ProviderType.OPENAI,
🤖 Fix all issues with AI agents
In `@backend/app/alembic/versions/041_extend_collection_table_for_provider_.py`:
- Line 29: Add explicit return type hints to the Alembic migration functions:
change the signature of upgrade() (and the corresponding downgrade() at the
other location) to include a return type of None (e.g., def upgrade() -> None:
and def downgrade() -> None:) so the functions conform to the repo’s Python
typing guidelines; ensure any other migration functions in this file follow the
same pattern.
- Around line 31-40: The migration adds a non-nullable "provider" column via
op.add_column and then executes an UPDATE, which fails on tables with rows
because there's no server default; modify the op.add_column call for "provider"
to include server_default="OPENAI" so the ALTER succeeds, then run the UPDATE
(op.execute) and finally remove the server default with an ALTER/sa.DDL or
op.alter_column to drop the server_default; also add return type hints -> None
to the upgrade() and downgrade() functions to match coding guidelines.
- Line 67: The database unique constraint on collection.name is too strict;
change the migration that calls op.create_unique_constraint(None, "collection",
["name"]) to instead create a composite unique constraint on ("project_id",
"name") so names are scoped per project, and update the Collection ORM model to
remove unique=True from the name column and add a composite unique constraint in
__table_args__ (e.g., UniqueConstraint("project_id", "name")). Also ensure the
exists_by_name method still filters by project_id and will now align with the DB
constraint.

In `@backend/app/crud/collection/collection.py`:
- Around line 96-104: The exists_by_name implementation is misaligned with the
DB uniqueness: Collection.name is globally unique and soft-deleted rows still
exist, so change the query in exists_by_name to check only Collection.name ==
collection_name (remove the .where(Collection.project_id == self.project_id) and
the .where(Collection.deleted_at.is_(None)) conditions) so it detects any
existing row (including soft-deleted or in other projects) and returns result is
not None; update the method that calls exists_by_name accordingly if callers
expect project-scoped behavior.

In `@backend/app/models/collection.py`:
- Around line 50-60: The model fields name and description are declared nullable
in the SQL model but typed as non-optional, causing validation errors when
omitted; update the type annotations for the Collection model's name and
description to be Optional[str] (or str | None) and ensure their Field
definitions keep nullable=True (or provide default=None) so Pydantic accepts
omission during CreationRequest validation; locate the name and description
Field declarations in collection.py to apply this change.
- Around line 31-37: The provider field definition is currently wrapped in a
tuple and uses a set for sa_column_kwargs which will break SQLAlchemy expansion;
update the provider annotation so it assigns Field(...) directly (not as a
tuple) and change sa_column_kwargs to a dict with the key "comment" containing
the column description (e.g. sa_column_kwargs={"comment": "LLM provider used for
this collection (e.g., 'openai', 'bedrock', 'gemini', etc)"}), keeping
ProviderType, Field, and nullable=False as shown so the model mapping works
correctly.

In `@backend/app/services/collections/create_collection.py`:
- Around line 135-143: The execute_job function declares an unused
Celery-provided parameter task_instance; add a type hint and rename it to
_task_instance (or _task_instance: Any) to indicate it's intentionally unused
and satisfy linters; update the function signature for execute_job accordingly
and import typing.Any if needed so references to execute_job and task_instance
clearly reflect the change.

In `@backend/app/services/collections/helpers.py`:
- Around line 20-27: The helper functions in this module need explicit return
type annotations per repo typing guidelines; update the function signatures
(e.g., get_service_name and the other helper functions defined around lines
116-131) to include explicit return types (for these helpers use -> str or the
correct concrete type) so the module is fully typed, and ensure any necessary
typing imports are added if required.

In `@backend/app/services/collections/providers/base.py`:
- Around line 22-28: The __init__ method in the provider base class is missing
an explicit return type; update the constructor signature for the class in
base.py from "def __init__(self, client: Any)" to include the explicit return
annotation "-> None" (i.e., def __init__(self, client: Any) -> None:) while
keeping the existing docstring and assignment to self.client to satisfy the
project's type-hinting rules.

In `@backend/app/tests/utils/llm_provider.py`:
- Around line 140-162: The function get_mock_provider lacks a return type
annotation; update its signature to include an explicit return type (e.g., ->
MagicMock) so the function complies with typing guidelines—modify the def
get_mock_provider(...) declaration to add the return type and ensure the import
for MagicMock (from unittest.mock import MagicMock) is available in the test
module if not already present.
♻️ Duplicate comments (1)
backend/app/services/collections/create_collection.py (1)

165-190: Provider create uses session-bound CRUD after session closes.

document_crud and storage are created in a session context, but provider.create(...) is invoked after the session exits. If provider logic reads documents via DocumentCrud, this can fail due to a closed session.

🐛 Proposed fix: keep provider.create inside the session scope
-        provider = get_llm_provider(
-            session=session,
-            provider=creation_request.provider,
-            project_id=project_id,
-            organization_id=organization_id,
-        )
-
-        result = provider.create(
-            collection_request=creation_request,
-            storage=storage,
-            document_crud=document_crud,
-        )
+        provider = get_llm_provider(
+            session=session,
+            provider=creation_request.provider,
+            project_id=project_id,
+            organization_id=organization_id,
+        )
+        result = provider.create(
+            collection_request=creation_request,
+            storage=storage,
+            document_crud=document_crud,
+        )
🧹 Nitpick comments (8)
backend/app/services/collections/providers/base.py (2)

31-52: Fix create docstring + error message to match signature/return type.

The docstring references args that don’t exist and returns llm_service_id/name even though the method returns Collection. The NotImplementedError also mentions “execute.” This can mislead provider implementers.

📝 Proposed cleanup
-        Args:
-            collection_request: Collection parameters (name, description, document list, etc.)
-            storage: Cloud storage instance for file access
-            document_crud: DocumentCrud instance for fetching documents
-            batch_size: Number of documents to process per batch
-            with_assistant: Whether to create an assistant/agent
-            assistant_options: Options for assistant creation (provider-specific)
+        Args:
+            collection_request: Collection parameters (name, description, document list, etc.)
+            storage: Cloud storage instance for file access
+            document_crud: DocumentCrud instance for fetching documents
...
-        Returns:
-            llm_service_id: ID of the resource to delete
-            llm_service_name: Name of the service (determines resource type)
+        Returns:
+            Collection created by the provider
         """
-        raise NotImplementedError("Providers must implement execute method")
+        raise NotImplementedError("Providers must implement create method")

53-74: Update delete/cleanup docstrings to match parameters.

Both methods take a Collection, but the docstrings still describe llm_service_id/name and “CreateCollectionresult.”

📝 Proposed cleanup
-        Args:
-            llm_service_id: ID of the resource to delete
-            llm_service_name: Name of the service (determines resource type)
+        Args:
+            collection: Collection record containing provider identifiers
...
-        Args:
-            collection_result: The CreateCollectionresult returned from execute, containing resource IDs
+        Args:
+            collection: Collection record containing provider identifiers to roll back
backend/app/tests/utils/collection.py (1)

27-50: Missing type hint for project parameter.

The project parameter should have a type hint for consistency with coding guidelines.

Proposed fix
+from app.models import Project
+
 def get_assistant_collection(
     db: Session,
-    project,
+    project: Project,
     *,
     assistant_id: Optional[str] = None,
     model: str = "gpt-4o",
     collection_id: Optional[UUID] = None,
 ) -> Collection:
backend/app/tests/crud/collections/collection/test_crud_collection_create.py (1)

18-24: Consider using ProviderType.OPENAI enum for consistency.

The test uses the string "OPENAI" directly, while other test utilities like get_assistant_collection use ProviderType.OPENAI. For type safety and consistency across the codebase, consider using the enum.

Proposed fix
+from app.models import ProviderType
+
 class TestCollectionCreate:
     _n_documents = 10

     `@openai_responses.mock`()
     def test_create_associates_documents(self, db: Session):
         project = get_project(db)
         collection = Collection(
             id=uuid4(),
             project_id=project.id,
             llm_service_id="asst_dummy",
             llm_service_name="gpt-4o",
-            provider="OPENAI",
+            provider=ProviderType.OPENAI,
         )
backend/app/services/collections/create_collection.py (1)

158-162: Use idiomatic truthiness for with_assistant.

Avoid explicit comparisons to True.

♻️ Proposed change
-        if (
-            with_assistant == True
-        ):  # this will be removed once dalgo switches to vector store creation only
+        if with_assistant:  # this will be removed once dalgo switches to vector store creation only
backend/app/services/collections/providers/openai.py (2)

19-21: Add explicit return type to __init__.

Helps enforce the “type hints everywhere” rule.

♻️ Proposed change
-    def __init__(self, client: OpenAI):
+    def __init__(self, client: OpenAI) -> None:
As per coding guidelines, add type hints for all parameters and return values.

44-47: Log prefixes should reflect the function name.

create() logs currently use [OpenAIProvider.execute] which breaks the logging convention.

🔧 Proposed change
-                "[OpenAIProvider.execute] Vector store created | "
+                "[OpenAIProvider.create] Vector store created | "
@@
-                    "[OpenAIProvider.execute] Assistant created | "
+                    "[OpenAIProvider.create] Assistant created | "
@@
-                    "[OpenAIProvider.execute] Skipping assistant creation | with_assistant=False"
+                    "[OpenAIProvider.create] Skipping assistant creation | with_assistant=False"
@@
-                f"[OpenAIProvider.execute] Failed to create collection: {str(e)}",
+                f"[OpenAIProvider.create] Failed to create collection: {str(e)}",
As per coding guidelines, prefix log messages with the function name in square brackets.

Also applies to: 68-71, 78-80, 88-90

backend/app/models/collection.py (1)

87-105: Add explicit return types for new helper methods.

model_post_init and extract_super_type are missing return annotations.

♻️ Proposed change
-from typing import Any, Literal
+from typing import Any, Iterable, Literal
@@
-    def model_post_init(self, __context: Any):
+    def model_post_init(self, __context: Any) -> None:
         self.documents = list(set(self.documents))
@@
-    def extract_super_type(self, cls: "CreationRequest"):
+    def extract_super_type(self, cls: "CreationRequest") -> Iterable[tuple[str, Any]]:
         for field_name in cls.model_fields.keys():
             field_value = getattr(self, field_name)
             yield (field_name, field_value)
As per coding guidelines, add type hints for all parameters and return values.

Also applies to: 179-188

)


def upgrade():
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add return type hints to migration functions.

Required by the repo’s Python typing guidelines.

✍️ Suggested update
-def upgrade():
+def upgrade() -> None:
@@
-def downgrade():
+def downgrade() -> None:

Also applies to: 74-74

🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/041_extend_collection_table_for_provider_.py` at
line 29, Add explicit return type hints to the Alembic migration functions:
change the signature of upgrade() (and the corresponding downgrade() at the
other location) to include a return type of None (e.g., def upgrade() -> None:
and def downgrade() -> None:) so the functions conform to the repo’s Python
typing guidelines; ensure any other migration functions in this file follow the
same pattern.

Comment on lines +31 to +40
op.add_column(
"collection",
sa.Column(
"provider",
provider_type,
nullable=False,
comment="LLM provider used for this collection",
),
)
op.execute("UPDATE collection SET provider = 'OPENAI' WHERE provider IS NULL")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "041_extend_collection_table_for_provider_*.py" -type f

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 150


🏁 Script executed:

find backend -name "*.py" -path "*alembic/versions*" | grep -i provider

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 225


🏁 Script executed:

ls -la backend/app/alembic/versions/ 2>/dev/null | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1587


🏁 Script executed:

find . -type d -name versions

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 105


🏁 Script executed:

git ls-files | grep -i alembic | grep -i version | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1328


🏁 Script executed:

find . -name "*041*" -type f 2>/dev/null

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 150


🏁 Script executed:

rg "extend_collection_table_for_provider" -l

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

rg "provider_type" -l | head -10

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 286


🏁 Script executed:

git ls-files | grep alembic

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2986


🏁 Script executed:

rg "op.add_column.*provider" -A 5 -B 2

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

rg 'UPDATE collection SET provider' -B 5 -A 2

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 923


🏁 Script executed:

ls -la

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1555


🏁 Script executed:

git ls-files | wc -l

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 78


🏁 Script executed:

git ls-files | grep -E "\.py$" | grep -i migration | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🌐 Web query:

Alembic add_column non-nullable without server_default existing rows failure

💡 Result:

Problem

  • Adding a NOT NULL column to a table with existing rows fails because existing rows would have NULL for the new column and the DB enforces NOT NULL. Alembic/autogenerate often emits op.add_column(... nullable=False) which triggers this. [1][2]

Safe solutions (pick one)

  1. Fast: add with a temporary server_default, then remove it
  • Add column with server_default so the DB populates existing rows at add-time, then ALTER to drop the default and set nullable=False.
  • PostgreSQL can optimize ADD COLUMN with a non-volatile DEFAULT so this is fast on modern versions; older PG may rewrite the table. Example:
    • op.add_column('t', sa.Column('col', sa.String(), nullable=False, server_default='x'))
    • op.alter_column('t', 'col', server_default=None)
  • References: Alembic docs; PostgreSQL behavior discussion. [2][3]
  1. Three-step (most portable / safest)
  • Step A: add nullable column:
    • op.add_column('t', sa.Column('col', sa.String(), nullable=True))
  • Step B: populate existing rows (UPDATE in migration, possibly batched):
    • op.execute("UPDATE t SET col = 'x' WHERE col IS NULL")
  • Step C: make column NOT NULL:
    • op.alter_column('t', 'col', nullable=False)
  • Use this for fine-grained control or when you need per-row logic and for DBs that don’t support fast server-default semantics. [1][4]
  1. Constraint/validation approach (avoid long exclusive locks)
  • Create a NOT VALID CHECK constraint enforcing col IS NOT NULL, update rows, then VALIDATE CONSTRAINT and drop the check if desired — reduces locking on large tables (Postgres-specific pattern). [5]

Notes and gotchas

  • SQLite requires batch mode (op.batch_alter_table) for some ALTERs. [2]
  • If you use autogenerate, manually edit the migration to add server_default or split into the three steps above.
  • After using a server_default to populate rows, remove the server_default if it was only intended for migration.

Concise Alembic examples

  • Server-default quick path:
    from alembic import op
    import sqlalchemy as sa

    def upgrade():
    op.add_column('users', sa.Column('plan', sa.String(), nullable=False, server_default='free'))
    op.alter_column('users', 'plan', server_default=None)

  • Three-step portable path:
    def upgrade():
    op.add_column('users', sa.Column('plan', sa.String(), nullable=True))
    op.execute("UPDATE users SET plan='free' WHERE plan IS NULL")
    op.alter_column('users', 'plan', nullable=False)

References
[1] Alembic operation docs (add_column / alter_column).
[2] Ross Gray — Alembic/Postgres defaults & performance (ADD COLUMN with DEFAULT optimization).
[3] Dev.to / blog posts showing server_default and three-step patterns.
[4] Community posts & examples resolving NotNull/IntegrityError when adding columns.
[5] Squawk docs — adding NOT NULL safely (NOT VALID check constraint pattern).

If you want, tell me your DB (Postgres/MySQL/SQLite) and I’ll produce a migration file tuned to it.


🏁 Script executed:

cat -n backend/app/alembic/versions/041_extend_collection_table_for_provider_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3797


🏁 Script executed:

wc -l backend/app/alembic/versions/041_extend_collection_table_for_provider_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 152


🏁 Script executed:

cat backend/app/alembic/versions/041_extend_collection_table_for_provider_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3034


🏁 Script executed:

cat backend/app/alembic/versions/012_added_provider_column_to_the_credential_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2468


🏁 Script executed:

head -100 backend/app/alembic/versions/041_extend_collection_table_for_provider_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2693


Fix migration failure risk: non-nullable provider column is added without a server default.

On tables with existing rows, the ADD COLUMN with nullable=False will fail immediately without a server default, preventing the subsequent UPDATE statement from executing. Add server_default="OPENAI" to the column definition, then remove the server default after the update to allow future inserts without a default value.

Additionally, the upgrade() and downgrade() functions lack return type hints. Add -> None return type annotations to both functions per the coding guidelines.

🔧 Suggested migration fix
     op.add_column(
         "collection",
         sa.Column(
             "provider",
             provider_type,
             nullable=False,
+            server_default="OPENAI",
             comment="LLM provider used for this collection",
         ),
     )
     op.execute("UPDATE collection SET provider = 'OPENAI' WHERE provider IS NULL")
+    op.alter_column(
+        "collection",
+        "provider",
+        server_default=None,
+        existing_type=provider_type,
+    )

Also add return type hints:

-def upgrade():
+def upgrade() -> None:

-def downgrade():
+def downgrade() -> None:
🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/041_extend_collection_table_for_provider_.py`
around lines 31 - 40, The migration adds a non-nullable "provider" column via
op.add_column and then executes an UPDATE, which fails on tables with rows
because there's no server default; modify the op.add_column call for "provider"
to include server_default="OPENAI" so the ALTER succeeds, then run the UPDATE
(op.execute) and finally remove the server default with an ALTER/sa.DDL or
op.alter_column to drop the server_default; also add return type hints -> None
to the upgrade() and downgrade() functions to match coding guidelines.

existing_comment="Name of the LLM service provider",
existing_nullable=False,
)
op.create_unique_constraint(None, "collection", ["name"])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f "041_extend_collection_table_for_provider_" backend/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 148


🏁 Script executed:

find . -path "*/alembic/versions/*" -name "*041*" -type f

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 150


🏁 Script executed:

rg "exists_by_name" --type py -A 5

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1138


🏁 Script executed:

cat -n backend/app/alembic/versions/041_extend_collection_table_for_provider_.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3797


🏁 Script executed:

rg "class Collection" --type py -A 20 backend/app/models/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 8074


🏁 Script executed:

cat -n backend/app/models/collection.py | head -60

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2376


🏁 Script executed:

rg "project_id" backend/app/models/collection.py -B 2 -A 2

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 338


🏁 Script executed:

cat -n backend/app/models/collection.py | sed -n '50,75p'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1212


🏁 Script executed:

rg "unique=True" backend/app/models/collection.py -B 5 -A 5

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 428


🏁 Script executed:

rg "Collection.name" --type py -B 3 -A 3 backend/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 656


🏁 Script executed:

rg "unique_together\|UniqueConstraint" --type py backend/app/models/collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

rg "Index\|unique" backend/app/models/collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

python3 << 'EOF'
# Check if there are any other constraints or indexes defined
import ast
import sys

with open('backend/app/models/collection.py', 'r') as f:
    content = f.read()
    
# Search for __table_args__ or similar constraint definitions
if '__table_args__' in content:
    print("Found __table_args__ definition")
    lines = content.split('\n')
    for i, line in enumerate(lines):
        if '__table_args__' in line:
            print(f"Line {i+1}: {line}")
            for j in range(1, 10):
                if i+j < len(lines):
                    print(f"Line {i+j+1}: {lines[i+j]}")
else:
    print("No __table_args__ found")

# Also check for other constraint patterns
if 'UniqueConstraint' in content or 'Index' in content:
    print("\nFound constraint/index patterns")
EOF

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 161


🏁 Script executed:

# Check if there's a previous migration that might have already defined a unique constraint differently
git log --oneline backend/app/alembic/versions/ | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 121


🏁 Script executed:

# Check for any other usages or assumptions about name uniqueness
rg "\.name" backend/app/crud/collection/collection.py -B 2 -A 2

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 286


🏁 Script executed:

# Verify the Collection model fully to see if unique=True constraint is defined at field level
sed -n '22,85p' backend/app/models/collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2448


🏁 Script executed:

# Check if there are any queries that might assume global name uniqueness
rg "filter.*name\|where.*name" backend/app/crud/collection/ --type py -B 1 -A 1

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


Unique constraint must include project_id to be project-scoped.

The exists_by_name method filters by project_id, allowing the same collection name to exist in different projects. However, the global unique constraint on name alone prevents this. The DB constraint contradicts the application logic and will reject valid use cases where different projects have collections with identical names.

Required constraint change
-    op.create_unique_constraint(None, "collection", ["name"])
+    op.create_unique_constraint(
+        "uq_collection_project_id_name", "collection", ["project_id", "name"]
+    )
-    op.drop_constraint("collection_name_key", "collection", type_="unique")
+    op.drop_constraint("uq_collection_project_id_name", "collection", type_="unique")

Also update the Collection model to remove unique=True from the name field (line 52) and add a composite unique constraint via __table_args__ if ORM-level validation is needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
op.create_unique_constraint(None, "collection", ["name"])
op.create_unique_constraint(
"uq_collection_project_id_name", "collection", ["project_id", "name"]
)
🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/041_extend_collection_table_for_provider_.py` at
line 67, The database unique constraint on collection.name is too strict; change
the migration that calls op.create_unique_constraint(None, "collection",
["name"]) to instead create a composite unique constraint on ("project_id",
"name") so names are scoped per project, and update the Collection ORM model to
remove unique=True from the name column and add a composite unique constraint in
__table_args__ (e.g., UniqueConstraint("project_id", "name")). Also ensure the
exists_by_name method still filters by project_id and will now align with the DB
constraint.

Comment on lines +96 to +104
def exists_by_name(self, collection_name: str) -> bool:
statement = (
select(Collection.id)
.where(Collection.project_id == self.project_id)
.where(Collection.name == collection_name)
.where(Collection.deleted_at.is_(None))
)
result = self.session.exec(statement).first()
return result is not None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align name existence check with DB uniqueness (soft deletes + cross‑project).

Collection.name is globally unique and soft-deleted rows still exist, but this check ignores other projects and deleted rows. That can return False even though the insert will fail with an IntegrityError, leading to misleading 409 handling or a later 500. Either (a) align the query with the current schema, or (b) change the DB constraint to match the intended scope/soft‑delete behavior.

✅ Option A: align with current global-unique constraint
-        statement = (
-            select(Collection.id)
-            .where(Collection.project_id == self.project_id)
-            .where(Collection.name == collection_name)
-            .where(Collection.deleted_at.is_(None))
-        )
+        statement = select(Collection.id).where(Collection.name == collection_name)
🤖 Prompt for AI Agents
In `@backend/app/crud/collection/collection.py` around lines 96 - 104, The
exists_by_name implementation is misaligned with the DB uniqueness:
Collection.name is globally unique and soft-deleted rows still exist, so change
the query in exists_by_name to check only Collection.name == collection_name
(remove the .where(Collection.project_id == self.project_id) and the
.where(Collection.deleted_at.is_(None)) conditions) so it detects any existing
row (including soft-deleted or in other projects) and returns result is not
None; update the method that calls exists_by_name accordingly if callers expect
project-scoped behavior.

Comment on lines +31 to +37
provider: ProviderType = (
Field(
nullable=False,
description="LLM provider used for this collection (e.g., 'openai', 'bedrock', 'gemini', etc)",
sa_column_kwargs={"LLM provider used for this collection"},
),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Provider field definition is invalid and will break model mapping.

The field is wrapped in a tuple and sa_column_kwargs is a set, which will raise when SQLAlchemy expands **sa_column_kwargs.

🐛 Proposed fix
-    provider: ProviderType = (
-        Field(
-            nullable=False,
-            description="LLM provider used for this collection (e.g., 'openai', 'bedrock', 'gemini', etc)",
-            sa_column_kwargs={"LLM provider used for this collection"},
-        ),
-    )
+    provider: ProviderType = Field(
+        nullable=False,
+        description="LLM provider used for this collection (e.g., 'openai', 'bedrock', 'gemini', etc)",
+        sa_column_kwargs={"comment": "LLM provider used for this collection"},
+    )
As per coding guidelines, use `sa_column_kwargs["comment"]` for column descriptions.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
provider: ProviderType = (
Field(
nullable=False,
description="LLM provider used for this collection (e.g., 'openai', 'bedrock', 'gemini', etc)",
sa_column_kwargs={"LLM provider used for this collection"},
),
)
provider: ProviderType = Field(
nullable=False,
description="LLM provider used for this collection (e.g., 'openai', 'bedrock', 'gemini', etc)",
sa_column_kwargs={"comment": "LLM provider used for this collection"},
)
🤖 Prompt for AI Agents
In `@backend/app/models/collection.py` around lines 31 - 37, The provider field
definition is currently wrapped in a tuple and uses a set for sa_column_kwargs
which will break SQLAlchemy expansion; update the provider annotation so it
assigns Field(...) directly (not as a tuple) and change sa_column_kwargs to a
dict with the key "comment" containing the column description (e.g.
sa_column_kwargs={"comment": "LLM provider used for this collection (e.g.,
'openai', 'bedrock', 'gemini', etc)"}), keeping ProviderType, Field, and
nullable=False as shown so the model mapping works correctly.

Comment on lines +50 to 60
name: str = Field(
nullable=True,
unique=True,
description="Name of the collection",
sa_column_kwargs={"comment": "Name of the collection"},
)
description: str = Field(
nullable=True,
description="Description of the collection",
sa_column_kwargs={"comment": "Description of the collection"},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

name/description are nullable but typed as non-optional.

This can raise validation errors when CreationRequest omits them (they’re optional there).

🐛 Proposed fix
-    name: str = Field(
-        nullable=True,
-        unique=True,
-        description="Name of the collection",
-        sa_column_kwargs={"comment": "Name of the collection"},
-    )
-    description: str = Field(
-        nullable=True,
-        description="Description of the collection",
-        sa_column_kwargs={"comment": "Description of the collection"},
-    )
+    name: str | None = Field(
+        default=None,
+        nullable=True,
+        unique=True,
+        description="Name of the collection",
+        sa_column_kwargs={"comment": "Name of the collection"},
+    )
+    description: str | None = Field(
+        default=None,
+        nullable=True,
+        description="Description of the collection",
+        sa_column_kwargs={"comment": "Description of the collection"},
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: str = Field(
nullable=True,
unique=True,
description="Name of the collection",
sa_column_kwargs={"comment": "Name of the collection"},
)
description: str = Field(
nullable=True,
description="Description of the collection",
sa_column_kwargs={"comment": "Description of the collection"},
)
name: str | None = Field(
default=None,
nullable=True,
unique=True,
description="Name of the collection",
sa_column_kwargs={"comment": "Name of the collection"},
)
description: str | None = Field(
default=None,
nullable=True,
description="Description of the collection",
sa_column_kwargs={"comment": "Description of the collection"},
)
🤖 Prompt for AI Agents
In `@backend/app/models/collection.py` around lines 50 - 60, The model fields name
and description are declared nullable in the SQL model but typed as
non-optional, causing validation errors when omitted; update the type
annotations for the Collection model's name and description to be Optional[str]
(or str | None) and ensure their Field definitions keep nullable=True (or
provide default=None) so Pydantic accepts omission during CreationRequest
validation; locate the name and description Field declarations in collection.py
to apply this change.

Comment on lines 135 to 143
def execute_job(
request: dict,
with_assistant: bool,
project_id: int,
organization_id: int,
task_id: str,
job_id: str,
with_assistant: bool,
task_instance,
) -> None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n backend/app/services/collections/create_collection.py | sed -n '130,160p'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1174


🏁 Script executed:

rg -A 150 "def execute_job" backend/app/services/collections/create_collection.py | grep -n "task_instance"

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 95


🏁 Script executed:

wc -l backend/app/services/collections/create_collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 132


🏁 Script executed:

cat -n backend/app/services/collections/create_collection.py | sed -n '135,272p' | head -100

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 4275


🏁 Script executed:

cat -n backend/app/services/collections/create_collection.py | sed -n '234,272p'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1731


🏁 Script executed:

rg "execute_job" backend/app/services/collections/create_collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 578


🏁 Script executed:

rg -B 5 'function_path="app.services.collections.create_collection.execute_job"' backend/app/services/collections/create_collection.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 303


🏁 Script executed:

rg -A 10 "def start_low_priority_job" backend/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 887


🏁 Script executed:

rg -A 30 "def start_low_priority_job" backend/app/celery/utils.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1110


🏁 Script executed:

rg -B 5 -A 30 "def execute_low_priority_task" backend/app/celery/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2673


🏁 Script executed:

rg -A 50 "def _execute_job_internal" backend/app/celery/tasks/job_execution.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1835


🏁 Script executed:

rg "from celery" backend/app/celery/tasks/job_execution.py | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 106


task_instance is untyped and unused.

This parameter is passed by the Celery task framework but not used within the function. Add a type hint and prefix with _ to suppress lint warnings.

🔧 Suggested tweak
-    task_instance,
+    _task_instance: object | None,
🧰 Tools
🪛 Ruff (0.14.13)

142-142: Unused function argument: task_instance

(ARG001)

🤖 Prompt for AI Agents
In `@backend/app/services/collections/create_collection.py` around lines 135 -
143, The execute_job function declares an unused Celery-provided parameter
task_instance; add a type hint and rename it to _task_instance (or
_task_instance: Any) to indicate it's intentionally unused and satisfy linters;
update the function signature for execute_job accordingly and import typing.Any
if needed so references to execute_job and task_instance clearly reflect the
change.

Comment on lines +20 to +27
def get_service_name(provider: str) -> str:
"""Get the collection service name for a provider."""
names = {
"openai": "openai vector store",
# "bedrock": "bedrock knowledge base",
# "gemini": "gemini file search store",
}
return names.get(provider.lower(), "")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add return type annotations to new helpers.

Required by repo typing guidelines.

✍️ Suggested update
-def get_service_name(provider: str) -> str:
+def get_service_name(provider: str) -> str:
@@
-def ensure_unique_name(
+def ensure_unique_name(
     session: SessionDep,
     project_id: int,
     requested_name: str,
-) -> str:
+) -> str:

Also applies to: 116-131

🤖 Prompt for AI Agents
In `@backend/app/services/collections/helpers.py` around lines 20 - 27, The helper
functions in this module need explicit return type annotations per repo typing
guidelines; update the function signatures (e.g., get_service_name and the other
helper functions defined around lines 116-131) to include explicit return types
(for these helpers use -> str or the correct concrete type) so the module is
fully typed, and ensure any necessary typing imports are added if required.

Comment on lines +22 to +28
def __init__(self, client: Any):
"""Initialize provider with client.

Args:
client: Provider-specific client instance
"""
self.client = client
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit return type for __init__.

Type hints are required for all parameters and return values; add -> None here. As per coding guidelines, ...

🔧 Suggested fix
-    def __init__(self, client: Any):
+    def __init__(self, client: Any) -> None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __init__(self, client: Any):
"""Initialize provider with client.
Args:
client: Provider-specific client instance
"""
self.client = client
def __init__(self, client: Any) -> None:
"""Initialize provider with client.
Args:
client: Provider-specific client instance
"""
self.client = client
🤖 Prompt for AI Agents
In `@backend/app/services/collections/providers/base.py` around lines 22 - 28, The
__init__ method in the provider base class is missing an explicit return type;
update the constructor signature for the class in base.py from "def
__init__(self, client: Any)" to include the explicit return annotation "-> None"
(i.e., def __init__(self, client: Any) -> None:) while keeping the existing
docstring and assignment to self.client to satisfy the project's type-hinting
rules.

Comment on lines +140 to +162
def get_mock_provider(
llm_service_id: str = "mock_service_id",
llm_service_name: str = "mock_service_name",
):
"""
Create a properly configured mock provider for tests.

Returns a mock that mimics BaseProvider with:
- create() method returning result with llm_service_id and llm_service_name
- cleanup() method for cleanup on failure
- delete() method for deletion
"""
mock_provider = MagicMock()

mock_result = MagicMock()
mock_result.llm_service_id = llm_service_id
mock_result.llm_service_name = llm_service_name

mock_provider.create.return_value = mock_result
mock_provider.cleanup = MagicMock()
mock_provider.delete = MagicMock()

return mock_provider
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a return type annotation for get_mock_provider.

This is required by the Python typing guideline.

✍️ Suggested update
-def get_mock_provider(
+def get_mock_provider(
     llm_service_id: str = "mock_service_id",
     llm_service_name: str = "mock_service_name",
-):
+) -> MagicMock:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_mock_provider(
llm_service_id: str = "mock_service_id",
llm_service_name: str = "mock_service_name",
):
"""
Create a properly configured mock provider for tests.
Returns a mock that mimics BaseProvider with:
- create() method returning result with llm_service_id and llm_service_name
- cleanup() method for cleanup on failure
- delete() method for deletion
"""
mock_provider = MagicMock()
mock_result = MagicMock()
mock_result.llm_service_id = llm_service_id
mock_result.llm_service_name = llm_service_name
mock_provider.create.return_value = mock_result
mock_provider.cleanup = MagicMock()
mock_provider.delete = MagicMock()
return mock_provider
def get_mock_provider(
llm_service_id: str = "mock_service_id",
llm_service_name: str = "mock_service_name",
) -> MagicMock:
"""
Create a properly configured mock provider for tests.
Returns a mock that mimics BaseProvider with:
- create() method returning result with llm_service_id and llm_service_name
- cleanup() method for cleanup on failure
- delete() method for deletion
"""
mock_provider = MagicMock()
mock_result = MagicMock()
mock_result.llm_service_id = llm_service_id
mock_result.llm_service_name = llm_service_name
mock_provider.create.return_value = mock_result
mock_provider.cleanup = MagicMock()
mock_provider.delete = MagicMock()
return mock_provider
🤖 Prompt for AI Agents
In `@backend/app/tests/utils/llm_provider.py` around lines 140 - 162, The function
get_mock_provider lacks a return type annotation; update its signature to
include an explicit return type (e.g., -> MagicMock) so the function complies
with typing guidelines—modify the def get_mock_provider(...) declaration to add
the return type and ensure the import for MagicMock (from unittest.mock import
MagicMock) is available in the test module if not already present.

avirajsingh7 and others added 5 commits January 20, 2026 09:59
…urity module and tests (#507)

API Key: remove API key encryption and decryption functions
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...
* first stab at refactoring

* cleanups

* run pre commit

* added missing permission imports

* refactoring batch code

* refactoring

* coderabbit suggestion changes

* coderabbit suggestion changes

* updated testcases

* following PEP 8 standards

* cleanup

* cleanup

* renaming endpoints with better semantics

* updated fail status

* cleanup delete dataset

* running pre commit

* updating testcases
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/app/tests/crud/evaluations/test_dataset.py (1)

11-20: Missing imports will cause test failures.

The new TestDeleteDataset class uses delete_dataset, EvaluationRun, and now which are not imported. Add these imports:

🐛 Proposed fix to add missing imports
 from app.crud.evaluations.dataset import (
     create_evaluation_dataset,
+    delete_dataset,
     download_csv_from_object_store,
     get_dataset_by_id,
     get_dataset_by_name,
     list_datasets,
     update_dataset_langfuse_id,
     upload_csv_to_object_store,
 )
-from app.models import Organization, Project
+from app.models import EvaluationRun, Organization, Project
+from app.core.util import now
backend/app/crud/evaluations/embeddings.py (1)

83-127: Prefix the remaining log lines in build_embedding_jsonl.
A few warnings and the final info log still lack the required [build_embedding_jsonl] prefix, which breaks log filtering consistency. As per coding guidelines.

♻️ Suggested patch
-        if not item_id:
-            logger.warning("Skipping result with no item_id")
+        if not item_id:
+            logger.warning("[build_embedding_jsonl] Skipping result with no item_id")
             continue
@@
-        if not trace_id:
-            logger.warning(f"Skipping item {item_id} - no trace_id found")
+        if not trace_id:
+            logger.warning(f"[build_embedding_jsonl] Skipping item {item_id} - no trace_id found")
             continue
@@
-        if not generated_output or not ground_truth:
-            logger.warning(f"Skipping item {item_id} - empty output or ground_truth")
+        if not generated_output or not ground_truth:
+            logger.warning(
+                f"[build_embedding_jsonl] Skipping item {item_id} - empty output or ground_truth"
+            )
             continue
@@
-    logger.info(f"Built {len(jsonl_data)} embedding JSONL lines")
+    logger.info(f"[build_embedding_jsonl] Built {len(jsonl_data)} embedding JSONL lines")
backend/app/tests/api/routes/test_evaluation.py (1)

495-507: Add type hints + return types for the untyped tests.
Two test functions are missing parameter and return annotations; the codebase requires typing on all function params/returns. As per coding guidelines.

🧩 Suggested patch
-    def test_upload_without_authentication(self, client, valid_csv_content):
+    def test_upload_without_authentication(
+        self, client: TestClient, valid_csv_content: str
+    ) -> None:
@@
-    def test_start_batch_evaluation_without_authentication(
-        self, client, sample_evaluation_config
-    ):
+    def test_start_batch_evaluation_without_authentication(
+        self, client: TestClient, sample_evaluation_config: dict[str, Any]
+    ) -> None:

Also applies to: 577-589

🤖 Fix all issues with AI agents
In @.github/workflows/cd-staging.yml:
- Around line 9-10: The staging CD job currently uses a hard disable (`if:
false`) on the `build` job which permanently prevents runs; replace this with a
toggleable condition using a repository or workflow variable (e.g., `if: ${{
vars.STAGING_CD_ENABLED == 'true' }}` or a workflow input) so the job can be
re-enabled without editing the YAML; update references to the `build` job and
document the repo variable in repo settings or the workflow inputs so
maintainers can flip the toggle as needed.

In `@backend/app/core/batch/polling.py`:
- Around line 44-47: The log shows the new status twice because
batch_job.provider_status is updated before logging; capture the previous status
into a local variable (e.g., old_status = batch_job.provider_status) before
calling update_batch_job, then call update_batch_job as before and change the
logger.info call to log f"{old_status} -> {provider_status}" (referencing
batch_job, provider_status, update_batch_job, and logger.info in polling.py) so
the message reflects the transition correctly.

In `@backend/app/services/evaluations/evaluation.py`:
- Around line 65-83: The current merge logic builds merged_config only from
three explicit fields, dropping any other valid config keys; change it to start
with a shallow copy of config (e.g., merged_config = dict(config)) and then fill
missing defaults from the assistant (use assistant.model,
assistant.instructions, assistant.temperature) so config keys take precedence;
for tools, if config contains an explicit "tools" key keep it, otherwise compute
vector_store_ids from config or assistant (vector_store_ids =
config.get("vector_store_ids", assistant.vector_store_ids or [])) and only then
set merged_config["tools"] to the file_search entry when no explicit tools were
provided; ensure this preserves other fields like "reasoning",
"max_output_tokens", and "response_format" while still filtering/normalizing any
non-OpenAI params as needed.

In `@backend/app/services/evaluations/validators.py`:
- Around line 92-97: The validation currently assumes file.content_type is
always set; first check if UploadFile.content_type is None and raise
HTTPException(status_code=422, detail="Missing Content-Type header or content
type not provided") to avoid the confusing "got: None" message, then normalize
the non-None content_type (e.g., lower-case and strip any charset portion by
splitting on ';') and compare the base type against ALLOWED_MIME_TYPES (use the
content_type variable and ALLOWED_MIME_TYPES identifiers) and raise the existing
422 with the clearer message if it still isn't allowed.

In `@backend/app/tests/api/routes/test_evaluation.py`:
- Line 1042: The file ends without a trailing newline causing Ruff W292; add a
single newline character at the end of the file so the final line (the assertion
line containing assert "not found" in error_str.lower()) is followed by a
newline, ensuring the file terminates with a newline.

In `@backend/app/tests/crud/evaluations/test_processing.py`:
- Line 805: Add a trailing newline at EOF of the test file so the last line
"mock_check.assert_called_once()" ends with a newline character; update
backend/app/tests/crud/evaluations/test_processing.py (ensuring the final token
mock_check.assert_called_once() is followed by a newline) to satisfy the W292
lint rule.
- Around line 259-423: The tests lack parameter and return type annotations on
async test functions and some patched mock parameters; update each async test
(e.g., test_process_completed_evaluation_success,
test_process_completed_evaluation_no_results,
test_process_completed_evaluation_no_batch_job_id) to include explicit parameter
types for patched mocks (use unittest.mock.MagicMock or AsyncMock as
appropriate) and add a return annotation -> None for each test and fixture
functions (apply same changes to other tests in the file around 484-805). Also
ensure the module imports the necessary typing and mock types (e.g., MagicMock,
AsyncMock, Any) so the new annotations resolve.
🧹 Nitpick comments (7)
backend/app/tests/utils/document.py (2)

3-3: Import Generator from collections.abc instead of typing.

Per PEP 585 and Python 3.9+, Generator should be imported from collections.abc rather than typing.

Suggested fix
-from typing import Any, Generator
+from typing import Any
+from collections.abc import Generator

165-165: Add trailing newline at end of file.

Static analysis flagged the missing newline at the end of the file.

Suggested fix
-        return result
+        return result
+
backend/app/tests/api/test_auth_failures.py (1)

41-47: Guard against 422s from body validation in auth-failure tests.

For POST/PATCH routes, {"name": "test"} may not satisfy required schemas; if body validation runs before auth, you’ll get 422 instead of 401. Consider using endpoint-specific payloads via factories/shared helpers to keep these tests stable across schema changes.
As per coding guidelines, use factory pattern for test fixtures.

backend/app/crud/evaluations/__init__.py (1)

1-37: LGTM! Expanded evaluation CRUD exports.

The new imports (save_score, fetch_trace_scores_from_langfuse) are confirmed to exist in their respective modules based on the relevant code snippets.

Consider adding an explicit __all__ list to declare the public API surface. This makes it clearer what symbols are intended for external use:

♻️ Optional: Add explicit __all__
__all__ = [
    "start_evaluation_batch",
    "create_evaluation_run",
    "get_evaluation_run_by_id",
    "list_evaluation_runs",
    "save_score",
    # ... other exports
]
backend/app/services/evaluations/__init__.py (1)

1-16: Consider adding __all__ to explicitly define the public API.

The module correctly consolidates exports from submodules. Adding an __all__ list would make the public API surface explicit and help tools like linters and IDEs understand intended exports.

♻️ Suggested addition
 """Evaluation services."""

 from app.services.evaluations.dataset import upload_dataset
 from app.services.evaluations.evaluation import (
     build_evaluation_config,
     get_evaluation_with_scores,
     start_evaluation,
 )
 from app.services.evaluations.validators import (
     ALLOWED_EXTENSIONS,
     ALLOWED_MIME_TYPES,
     MAX_FILE_SIZE,
     parse_csv_items,
     sanitize_dataset_name,
     validate_csv_file,
 )
+
+__all__ = [
+    "upload_dataset",
+    "build_evaluation_config",
+    "get_evaluation_with_scores",
+    "start_evaluation",
+    "ALLOWED_EXTENSIONS",
+    "ALLOWED_MIME_TYPES",
+    "MAX_FILE_SIZE",
+    "parse_csv_items",
+    "sanitize_dataset_name",
+    "validate_csv_file",
+]
backend/app/api/routes/evaluations/evaluation.py (1)

70-97: Consider bounding limit/offset to avoid unbounded scans.
Adding basic Query constraints helps prevent abuse and accidental large queries (verify desired bounds).

🔧 Suggested tweak
-    limit: int = 50,
-    offset: int = 0,
+    limit: int = Query(50, ge=1, le=100),
+    offset: int = Query(0, ge=0),
backend/app/api/routes/evaluations/dataset.py (1)

56-86: Add logging for the upload operation.

Per coding guidelines, log messages should be prefixed with the function name. Consider adding logging similar to the other endpoints (get_dataset, delete_dataset) for consistency and observability.

Suggested improvement
 async def upload_dataset(
     _session: SessionDep,
     auth_context: AuthContextDep,
     file: UploadFile = File(
         ..., description="CSV file with 'question' and 'answer' columns"
     ),
     dataset_name: str = Form(..., description="Name for the dataset"),
     description: str | None = Form(None, description="Optional dataset description"),
     duplication_factor: int = Form(
         default=1,
         ge=1,
         le=5,
         description="Number of times to duplicate each item (min: 1, max: 5)",
     ),
 ) -> APIResponse[DatasetUploadResponse]:
     """Upload an evaluation dataset."""
+    logger.info(
+        f"[upload_dataset] Uploading dataset | name={dataset_name} | "
+        f"org_id={auth_context.organization_.id} | "
+        f"project_id={auth_context.project_.id}"
+    )
+
     # Validate and read CSV file
     csv_content = await validate_csv_file(file)

Comment on lines 9 to +10
build:
if: false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/cd-staging.yml

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1676


🏁 Script executed:

find .github/workflows -type f -name "*.yml" -o -name "*.yaml"

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 220


🏁 Script executed:

cat -n .github/workflows/cd-production.yml

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1794


🏁 Script executed:

git log -p --follow -S "if: false" -- .github/workflows/cd-staging.yml | head -100

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

git log --oneline .github/workflows/cd-staging.yml | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 110


🏁 Script executed:

git show 23c9685:.github/workflows/cd-staging.yml | head -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1354


🏁 Script executed:

git log --all --oneline -- .github/workflows/cd-staging.yml | cat

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 146


🏁 Script executed:

git show ef56025:.github/workflows/cd-staging.yml 2>/dev/null | head -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1354


🏁 Script executed:

git log --all --reverse --oneline -- .github/workflows/cd-staging.yml

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 146


🏁 Script executed:

git show ef56025 --stat | head -20

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 475


🏁 Script executed:

git log --all --reverse --oneline | head -5

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 146


🏁 Script executed:

grep -r "staging" README.md 2>/dev/null || echo "No README found or no staging mention"

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 420


Staging CD job is disabled; confirm intent and consider using a toggleable condition.

if: false at line 10 disables the entire staging deployment job for all main branch pushes. If this is intentional but temporary, use a repository variable to make the toggle explicit and allow re-enabling without code changes.

Suggested toggle via repo variable
 jobs:
   build:
-    if: false
+    if: ${{ vars.ENABLE_STAGING_CD == 'true' }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
build:
if: false
build:
if: ${{ vars.ENABLE_STAGING_CD == 'true' }}
🧰 Tools
🪛 actionlint (1.7.10)

10-10: constant expression "false" in condition. remove the if: section

(if-cond)

🤖 Prompt for AI Agents
In @.github/workflows/cd-staging.yml around lines 9 - 10, The staging CD job
currently uses a hard disable (`if: false`) on the `build` job which permanently
prevents runs; replace this with a toggleable condition using a repository or
workflow variable (e.g., `if: ${{ vars.STAGING_CD_ENABLED == 'true' }}` or a
workflow input) so the job can be re-enabled without editing the YAML; update
references to the `build` job and document the repo variable in repo settings or
the workflow inputs so maintainers can flip the toggle as needed.

Comment on lines +44 to +47
logger.info(
f"[poll_batch_status] Updated | id={batch_job.id} | "
f"{batch_job.provider_status} -> {provider_status}"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Log message displays incorrect status transition.

After update_batch_job is called on line 40-42, batch_job.provider_status is updated to the new status. The log at line 46 therefore logs {new_status} -> {new_status} instead of the intended {old_status} -> {new_status}.

🐛 Proposed fix
+        old_status = batch_job.provider_status
+
         provider_status = status_result["provider_status"]
-        if provider_status != batch_job.provider_status:
+        if provider_status != old_status:
             update_data = {"provider_status": provider_status}

             if status_result.get("provider_output_file_id"):
@@ -44,7 +46,7 @@

             logger.info(
                 f"[poll_batch_status] Updated | id={batch_job.id} | "
-                f"{batch_job.provider_status} -> {provider_status}"
+                f"{old_status} -> {provider_status}"
             )
🤖 Prompt for AI Agents
In `@backend/app/core/batch/polling.py` around lines 44 - 47, The log shows the
new status twice because batch_job.provider_status is updated before logging;
capture the previous status into a local variable (e.g., old_status =
batch_job.provider_status) before calling update_batch_job, then call
update_batch_job as before and change the logger.info call to log f"{old_status}
-> {provider_status}" (referencing batch_job, provider_status, update_batch_job,
and logger.info in polling.py) so the message reflects the transition correctly.

Comment on lines +65 to +83
# Build config from assistant (use provided config values to override if present)
merged_config = {
"model": config.get("model", assistant.model),
"instructions": config.get("instructions", assistant.instructions),
"temperature": config.get("temperature", assistant.temperature),
}

# Add tools if vector stores are available
vector_store_ids = config.get(
"vector_store_ids", assistant.vector_store_ids or []
)
if vector_store_ids and len(vector_store_ids) > 0:
merged_config["tools"] = [
{
"type": "file_search",
"vector_store_ids": vector_store_ids,
}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Merge drops non-core config keys when assistant_id is used.
When an assistant is provided, only model/instructions/temperature (plus vector_store_ids → tools) are preserved. Any additional valid config fields (e.g., reasoning, max_output_tokens, response_format, explicit tools) are silently discarded, which conflicts with the “config values take precedence” docstring and can break evaluation behavior.

✅ Suggested fix (preserve config keys; filter non-OpenAI params)
-        merged_config = {
-            "model": config.get("model", assistant.model),
-            "instructions": config.get("instructions", assistant.instructions),
-            "temperature": config.get("temperature", assistant.temperature),
-        }
+        merged_config = {
+            "model": assistant.model,
+            "instructions": assistant.instructions,
+            "temperature": assistant.temperature,
+        }
+        # Overlay provided config while filtering out non-OpenAI params
+        merged_config.update(
+            {key: value for key, value in config.items() if key != "vector_store_ids"}
+        )
@@
-        vector_store_ids = config.get(
-            "vector_store_ids", assistant.vector_store_ids or []
-        )
-        if vector_store_ids and len(vector_store_ids) > 0:
+        vector_store_ids = config.get(
+            "vector_store_ids", assistant.vector_store_ids or []
+        )
+        if vector_store_ids and "tools" not in merged_config:
             merged_config["tools"] = [
                 {
                     "type": "file_search",
                     "vector_store_ids": vector_store_ids,
                 }
             ]
🤖 Prompt for AI Agents
In `@backend/app/services/evaluations/evaluation.py` around lines 65 - 83, The
current merge logic builds merged_config only from three explicit fields,
dropping any other valid config keys; change it to start with a shallow copy of
config (e.g., merged_config = dict(config)) and then fill missing defaults from
the assistant (use assistant.model, assistant.instructions,
assistant.temperature) so config keys take precedence; for tools, if config
contains an explicit "tools" key keep it, otherwise compute vector_store_ids
from config or assistant (vector_store_ids = config.get("vector_store_ids",
assistant.vector_store_ids or [])) and only then set merged_config["tools"] to
the file_search entry when no explicit tools were provided; ensure this
preserves other fields like "reasoning", "max_output_tokens", and
"response_format" while still filtering/normalizing any non-OpenAI params as
needed.

Comment on lines +92 to +97
content_type = file.content_type
if content_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=422,
detail=f"Invalid content type. Expected CSV, got: {content_type}",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle potential None content type.

UploadFile.content_type can be None when the client doesn't provide a Content-Type header. The current check will fail with a confusing error message ("Expected CSV, got: None").

Proposed fix
     content_type = file.content_type
-    if content_type not in ALLOWED_MIME_TYPES:
+    if not content_type or content_type not in ALLOWED_MIME_TYPES:
         raise HTTPException(
             status_code=422,
-            detail=f"Invalid content type. Expected CSV, got: {content_type}",
+            detail=f"Invalid content type. Expected CSV, got: {content_type or 'unknown'}",
         )
🤖 Prompt for AI Agents
In `@backend/app/services/evaluations/validators.py` around lines 92 - 97, The
validation currently assumes file.content_type is always set; first check if
UploadFile.content_type is None and raise HTTPException(status_code=422,
detail="Missing Content-Type header or content type not provided") to avoid the
confusing "got: None" message, then normalize the non-None content_type (e.g.,
lower-case and strip any charset portion by splitting on ';') and compare the
base type against ALLOWED_MIME_TYPES (use the content_type variable and
ALLOWED_MIME_TYPES identifiers) and raise the existing 422 with the clearer
message if it still isn't allowed.

error_str = response_data.get(
"detail", response_data.get("error", str(response_data))
)
assert "not found" in error_str.lower() No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add trailing newline (W292).
Ruff flagged a missing newline at EOF.

🧰 Tools
🪛 Ruff (0.14.13)

1042-1042: No newline at end of file

Add trailing newline

(W292)

🤖 Prompt for AI Agents
In `@backend/app/tests/api/routes/test_evaluation.py` at line 1042, The file ends
without a trailing newline causing Ruff W292; add a single newline character at
the end of the file so the final line (the assertion line containing assert "not
found" in error_str.lower()) is followed by a newline, ensuring the file
terminates with a newline.

Comment on lines +259 to +423
@pytest.fixture
def eval_run_with_batch(self, db: Session, test_dataset) -> EvaluationRun:
"""Create evaluation run with batch job."""
# Create batch job
batch_job = BatchJob(
provider="openai",
provider_batch_id="batch_abc123",
provider_status="completed",
job_type="evaluation",
total_items=2,
status="submitted",
organization_id=test_dataset.organization_id,
project_id=test_dataset.project_id,
inserted_at=now(),
updated_at=now(),
)
db.add(batch_job)
db.commit()
db.refresh(batch_job)

eval_run = create_evaluation_run(
session=db,
run_name="test_run",
dataset_name=test_dataset.name,
dataset_id=test_dataset.id,
config={"model": "gpt-4o"},
organization_id=test_dataset.organization_id,
project_id=test_dataset.project_id,
)
eval_run.batch_job_id = batch_job.id
eval_run.status = "processing"
db.add(eval_run)
db.commit()
db.refresh(eval_run)

return eval_run

@pytest.mark.asyncio
@patch("app.crud.evaluations.processing.download_batch_results")
@patch("app.crud.evaluations.processing.fetch_dataset_items")
@patch("app.crud.evaluations.processing.create_langfuse_dataset_run")
@patch("app.crud.evaluations.processing.start_embedding_batch")
@patch("app.crud.evaluations.processing.upload_batch_results_to_object_store")
async def test_process_completed_evaluation_success(
self,
mock_upload,
mock_start_embedding,
mock_create_langfuse,
mock_fetch_dataset,
mock_download,
db: Session,
eval_run_with_batch,
):
"""Test successfully processing completed evaluation."""
# Mock batch results
mock_download.return_value = [
{
"custom_id": "item1",
"response": {
"body": {
"id": "resp_123",
"output": "Answer 1",
"usage": {"total_tokens": 10},
}
},
}
]

# Mock dataset items
mock_fetch_dataset.return_value = [
{
"id": "item1",
"input": {"question": "Q1"},
"expected_output": {"answer": "A1"},
}
]

# Mock Langfuse
mock_create_langfuse.return_value = {"item1": "trace_123"}

# Mock embedding batch
mock_start_embedding.return_value = eval_run_with_batch

# Mock upload
mock_upload.return_value = "s3://bucket/results.jsonl"

mock_openai = MagicMock()
mock_langfuse = MagicMock()

result = await process_completed_evaluation(
eval_run=eval_run_with_batch,
session=db,
openai_client=mock_openai,
langfuse=mock_langfuse,
)

assert result is not None
mock_download.assert_called_once()
mock_fetch_dataset.assert_called_once()
mock_create_langfuse.assert_called_once()
mock_start_embedding.assert_called_once()

@pytest.mark.asyncio
@patch("app.crud.evaluations.processing.download_batch_results")
@patch("app.crud.evaluations.processing.fetch_dataset_items")
async def test_process_completed_evaluation_no_results(
self,
mock_fetch_dataset,
mock_download,
db: Session,
eval_run_with_batch,
):
"""Test processing with no valid results."""
mock_download.return_value = []
mock_fetch_dataset.return_value = [
{
"id": "item1",
"input": {"question": "Q1"},
"expected_output": {"answer": "A1"},
}
]

mock_openai = MagicMock()
mock_langfuse = MagicMock()

result = await process_completed_evaluation(
eval_run=eval_run_with_batch,
session=db,
openai_client=mock_openai,
langfuse=mock_langfuse,
)

db.refresh(result)
assert result.status == "failed"
assert "No valid results" in result.error_message

@pytest.mark.asyncio
async def test_process_completed_evaluation_no_batch_job_id(
self, db: Session, test_dataset
):
"""Test processing without batch_job_id."""
eval_run = create_evaluation_run(
session=db,
run_name="test_run",
dataset_name=test_dataset.name,
dataset_id=test_dataset.id,
config={"model": "gpt-4o"},
organization_id=test_dataset.organization_id,
project_id=test_dataset.project_id,
)

mock_openai = MagicMock()
mock_langfuse = MagicMock()

result = await process_completed_evaluation(
eval_run=eval_run,
session=db,
openai_client=mock_openai,
langfuse=mock_langfuse,
)

db.refresh(result)
assert result.status == "failed"
assert "no batch_job_id" in result.error_message

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add type hints + return annotations across fixtures/async tests.
Several fixtures and async tests omit parameter types (e.g., patched mocks/fixtures) and -> None return annotations. Please apply consistent typing throughout this module. As per coding guidelines.

🧩 Example updates
-    def eval_run_with_batch(self, db: Session, test_dataset) -> EvaluationRun:
+    def eval_run_with_batch(
+        self, db: Session, test_dataset: EvaluationDataset
+    ) -> EvaluationRun:
@@
-    async def test_process_completed_evaluation_success(
-        self,
-        mock_upload,
-        mock_start_embedding,
-        mock_create_langfuse,
-        mock_fetch_dataset,
-        mock_download,
-        db: Session,
-        eval_run_with_batch,
-    ):
+    async def test_process_completed_evaluation_success(
+        self,
+        mock_upload: MagicMock,
+        mock_start_embedding: MagicMock,
+        mock_create_langfuse: MagicMock,
+        mock_fetch_dataset: MagicMock,
+        mock_download: MagicMock,
+        db: Session,
+        eval_run_with_batch: EvaluationRun,
+    ) -> None:

Also applies to: 484-805

🤖 Prompt for AI Agents
In `@backend/app/tests/crud/evaluations/test_processing.py` around lines 259 -
423, The tests lack parameter and return type annotations on async test
functions and some patched mock parameters; update each async test (e.g.,
test_process_completed_evaluation_success,
test_process_completed_evaluation_no_results,
test_process_completed_evaluation_no_batch_job_id) to include explicit parameter
types for patched mocks (use unittest.mock.MagicMock or AsyncMock as
appropriate) and add a return annotation -> None for each test and fixture
functions (apply same changes to other tests in the file around 484-805). Also
ensure the module imports the necessary typing and mock types (e.g., MagicMock,
AsyncMock, Any) so the new annotations resolve.


assert result["total"] == 1
assert result["still_processing"] == 1
mock_check.assert_called_once() No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add trailing newline (W292).
Ruff flagged a missing newline at EOF.

🧰 Tools
🪛 Ruff (0.14.13)

805-805: No newline at end of file

Add trailing newline

(W292)

🤖 Prompt for AI Agents
In `@backend/app/tests/crud/evaluations/test_processing.py` at line 805, Add a
trailing newline at EOF of the test file so the last line
"mock_check.assert_called_once()" ends with a newline character; update
backend/app/tests/crud/evaluations/test_processing.py (ensuring the final token
mock_check.assert_called_once() is followed by a newline) to satisfy the W292
lint rule.

@nishika26 nishika26 closed this Jan 20, 2026
@nishika26 nishika26 deleted the enhancement/collection_provider_agnostic branch January 20, 2026 06:04
@nishika26
Copy link
Copy Markdown
Collaborator Author

Had to close this PR because recent merges to main caused many merge conflict because of which this PR became really messy

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Collections: making this module llm provider agnostic

3 participants