Collection: making the module provider agnostic#508
Conversation
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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: PotentialNameErrorifCreationRequestparsing fails.If
CreationRequest(**request)on line 156 raises an exception,creation_requestis never assigned. The check on line 269 will then raise aNameError.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
ValueErrorfor 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_namebackend/app/services/collections/providers/base.py (3)
30-53: Docstring parameters don't match the method signature.The docstring mentions
batch_size,with_assistant, andassistant_optionsparameters 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_idandllm_service_nameas parameters, but the actual signature only acceptscollection: 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 IDsbackend/app/services/collections/create_collection.py (1)
35-42: Unusedwith_assistantparameter.The
with_assistantparameter is accepted but never used instart_job. The assistant creation logic is now determined by checkingmodelandinstructionsin 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
Anytype is imported but not used in this file.Proposed fix
import logging -from typing import Any from openai import OpenAI
24-26: Redundantself.clientassignment.
super().__init__(client)already assignsself.client = clientinBaseProvider.__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 namedcreate. 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. Aforloop orcollections.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): + passbackend/app/services/collections/providers/registry.py (1)
61-69: Unreachable else branch and logging format.The
elsebranch (lines 65-69) is unreachable becauseLLMProvider.get(provider)on line 47 already raisesValueErrorfor 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: Addproviderfield toCollectionPublic.The
Collectiondatabase model includes aproviderfield (ProviderType enum) that represents the LLM provider (e.g., "openai"). This field is missing fromCollectionPublicand should be exposed in the response schema. Per learnings,providerandllm_service_nameserve different purposes—providerindicates the LLM provider name whilellm_service_namespecifies 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
📒 Files selected for processing (15)
backend/app/alembic/versions/041_adding_blob_column_in_collection_table.pybackend/app/models/__init__.pybackend/app/models/collection/__init__.pybackend/app/models/collection/request.pybackend/app/models/collection/response.pybackend/app/services/collections/create_collection.pybackend/app/services/collections/delete_collection.pybackend/app/services/collections/helpers.pybackend/app/services/collections/providers/__init__.pybackend/app/services/collections/providers/base.pybackend/app/services/collections/providers/openai.pybackend/app/services/collections/providers/registry.pybackend/app/tests/api/routes/collections/test_collection_info.pybackend/app/tests/api/routes/collections/test_collection_list.pybackend/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.pybackend/app/services/collections/providers/openai.pybackend/app/services/collections/providers/base.pybackend/app/services/collections/providers/registry.pybackend/app/services/collections/create_collection.pybackend/app/services/collections/providers/__init__.pybackend/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.pybackend/app/services/collections/providers/openai.pybackend/app/services/collections/providers/base.pybackend/app/services/collections/providers/registry.pybackend/app/tests/utils/collection.pybackend/app/tests/api/routes/collections/test_collection_list.pybackend/app/services/collections/create_collection.pybackend/app/services/collections/providers/__init__.pybackend/app/services/collections/helpers.pybackend/app/models/collection/__init__.pybackend/app/models/collection/response.pybackend/app/alembic/versions/041_adding_blob_column_in_collection_table.pybackend/app/tests/api/routes/collections/test_collection_info.pybackend/app/models/__init__.pybackend/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.pybackend/app/tests/api/routes/collections/test_collection_list.pybackend/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__.pybackend/app/models/collection/response.pybackend/app/models/__init__.pybackend/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.pybackend/app/services/collections/providers/openai.pybackend/app/services/collections/providers/registry.pybackend/app/tests/utils/collection.pybackend/app/tests/api/routes/collections/test_collection_list.pybackend/app/services/collections/providers/__init__.pybackend/app/services/collections/helpers.pybackend/app/alembic/versions/041_adding_blob_column_in_collection_table.pybackend/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__.pybackend/app/models/collection/response.pybackend/app/models/__init__.pybackend/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_initcorrectly removes duplicate documents by ID while preserving order.
214-221: LGTM!The
normalize_providervalidator correctly handles case-insensitive provider matching by normalizing to lowercase before validation.
224-243: LGTM!The
CreationRequestandDeletionRequestmodels are well-structured, properly compose their parent classes, and provide clear field descriptions.
35-47: ENUM type name is consistent. Theproviderfield correctly usesname="providertype"(lowercase) in both the model definition and the Alembic migration at041_adding_blob_column_in_collection_table.py. Thecreate_typedifference is intentional—the migration usescreate_type=Trueto create the type, while the model usescreate_type=Falseto use the existing type.backend/app/tests/api/routes/collections/test_collection_list.py (2)
10-10: LGTM!Importing
get_service_namefrom 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_collectionfunction correctly setsprovider=ProviderType.OPENAIon the created Collection, aligning with the new provider-based model.
67-75: LGTM!The
get_vector_store_collectionfunction correctly uses bothget_service_name("openai")for the service name andProviderType.OPENAIfor 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:
- Add column as nullable
- Backfill existing rows with default value
- 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_nameutility 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!
CreateCollectionResultandCollectionIDPublicmodels are well-defined with proper type hints.
32-33: LGTM!
CollectionWithDocsPubliccorrectly extendsCollectionPublicwith 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
providerandresultare 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
deleteandcleanupmethods 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
LLMProviderregistry 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.
| 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") |
There was a problem hiding this comment.
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.
| 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", | ||
| ) |
There was a problem hiding this comment.
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.
| name: str | None = Field( | ||
| min_length=1, | ||
| description="Name of the knowledge base to create or update", | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and read the file to see the actual context
fd backend/app/models/collection/request.py --type fRepository: 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
fiRepository: 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.pyRepository: 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.pyRepository: 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}')
EOFRepository: 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.pyRepository: 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 -20Repository: 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.
| result = provider.create( | ||
| collection_request=creation_request, | ||
| storage=storage, | ||
| document_crud=document_crud, | ||
| ) |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
🧩 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 +150Repository: 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.pyRepository: 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 -B3Repository: 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 -B2Repository: 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.pyRepository: 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" -A10Repository: 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" -A20Repository: 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.
There was a problem hiding this comment.
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_collectionintest_crud_collection_read_one.py:
get_assistant_collectionalready persists the collection, thencrud.create(collection, documents)at line 26 attempts to create it again.- The
clientvariable on line 19 is unused.- 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_idbackend/app/tests/utils/collection.py (1)
53-75: Bug:get_service_nameignores theproviderparameter.Line 71 calls
get_service_name("openai")with a hardcoded string, ignoring theproviderparameter 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 useProviderTypeenum for type safety and consistency withget_assistant_collection.- Missing type hint for
projectparameter.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
-> Collectionto the function signature per coding guidelines.The
clientvariable is necessary for OpenAI mock router initialization within thewith openai_mock.router:context and should be kept. The doublecreate()call works as intended—the second call triggersIntegrityErrorhandling which returns the existing collection, then documents are associated viaDocumentCollectionCrud.backend/app/tests/crud/collections/collection/test_crud_collection_delete.py (1)
15-34: Add full type hints and preferProviderTypeenum.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_crudandstorageare created in a session context, butprovider.create(...)is invoked after the session exits. If provider logic reads documents viaDocumentCrud, 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: Fixcreatedocstring + error message to match signature/return type.The docstring references args that don’t exist and returns
llm_service_id/nameeven though the method returnsCollection. TheNotImplementedErroralso 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 describellm_service_id/nameand “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 backbackend/app/tests/utils/collection.py (1)
27-50: Missing type hint forprojectparameter.The
projectparameter 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 usingProviderType.OPENAIenum for consistency.The test uses the string
"OPENAI"directly, while other test utilities likeget_assistant_collectionuseProviderType.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 forwith_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 onlybackend/app/services/collections/providers/openai.py (2)
19-21: Add explicit return type to__init__.Helps enforce the “type hints everywhere” rule.
As per coding guidelines, add type hints for all parameters and return values.♻️ Proposed change
- def __init__(self, client: OpenAI): + def __init__(self, client: OpenAI) -> None:
44-47: Log prefixes should reflect the function name.
create()logs currently use[OpenAIProvider.execute]which breaks the logging convention.As per coding guidelines, prefix log messages with the function name in square brackets.🔧 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)}",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_initandextract_super_typeare missing return annotations.As per coding guidelines, add type hints for all parameters and return values.♻️ 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)Also applies to: 179-188
| ) | ||
|
|
||
|
|
||
| def upgrade(): |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "041_extend_collection_table_for_provider_*.py" -type fRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 150
🏁 Script executed:
find backend -name "*.py" -path "*alembic/versions*" | grep -i providerRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 225
🏁 Script executed:
ls -la backend/app/alembic/versions/ 2>/dev/null | head -20Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1587
🏁 Script executed:
find . -type d -name versionsRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 105
🏁 Script executed:
git ls-files | grep -i alembic | grep -i version | head -20Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1328
🏁 Script executed:
find . -name "*041*" -type f 2>/dev/nullRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 150
🏁 Script executed:
rg "extend_collection_table_for_provider" -lRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 57
🏁 Script executed:
rg "provider_type" -l | head -10Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 286
🏁 Script executed:
git ls-files | grep alembicRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 2986
🏁 Script executed:
rg "op.add_column.*provider" -A 5 -B 2Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 57
🏁 Script executed:
rg 'UPDATE collection SET provider' -B 5 -A 2Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 923
🏁 Script executed:
ls -laRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 1555
🏁 Script executed:
git ls-files | wc -lRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 78
🏁 Script executed:
git ls-files | grep -E "\.py$" | grep -i migration | head -20Repository: 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)
- 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]
- 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]
- 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 sadef 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_.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 3797
🏁 Script executed:
wc -l backend/app/alembic/versions/041_extend_collection_table_for_provider_.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 152
🏁 Script executed:
cat backend/app/alembic/versions/041_extend_collection_table_for_provider_.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 3034
🏁 Script executed:
cat backend/app/alembic/versions/012_added_provider_column_to_the_credential_.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 2468
🏁 Script executed:
head -100 backend/app/alembic/versions/041_extend_collection_table_for_provider_.pyRepository: 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"]) |
There was a problem hiding this comment.
🧩 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 fRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 150
🏁 Script executed:
rg "exists_by_name" --type py -A 5Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1138
🏁 Script executed:
cat -n backend/app/alembic/versions/041_extend_collection_table_for_provider_.pyRepository: 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 -60Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 2376
🏁 Script executed:
rg "project_id" backend/app/models/collection.py -B 2 -A 2Repository: 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 5Repository: 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.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 57
🏁 Script executed:
rg "Index\|unique" backend/app/models/collection.pyRepository: 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")
EOFRepository: 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 -20Repository: 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 2Repository: 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.pyRepository: 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 1Repository: 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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
| 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"}, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
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"},
+ )📝 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.
| 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.
| 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"}, | ||
| ) |
There was a problem hiding this comment.
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.
| 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.
| 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: |
There was a problem hiding this comment.
🧩 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.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 132
🏁 Script executed:
cat -n backend/app/services/collections/create_collection.py | sed -n '135,272p' | head -100Repository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 1835
🏁 Script executed:
rg "from celery" backend/app/celery/tasks/job_execution.py | head -20Repository: 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.
| 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(), "") |
There was a problem hiding this comment.
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.
| def __init__(self, client: Any): | ||
| """Initialize provider with client. | ||
|
|
||
| Args: | ||
| client: Provider-specific client instance | ||
| """ | ||
| self.client = client |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
…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
There was a problem hiding this comment.
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
TestDeleteDatasetclass usesdelete_dataset,EvaluationRun, andnowwhich 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 nowbackend/app/crud/evaluations/embeddings.py (1)
83-127: Prefix the remaining log lines inbuild_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: ImportGeneratorfromcollections.abcinstead oftyping.Per PEP 585 and Python 3.9+,
Generatorshould be imported fromcollections.abcrather thantyping.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 boundinglimit/offsetto avoid unbounded scans.
Adding basicQueryconstraints 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)
| build: | ||
| if: false |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/cd-staging.ymlRepository: 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.ymlRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 1794
🏁 Script executed:
git log -p --follow -S "if: false" -- .github/workflows/cd-staging.yml | head -100Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 57
🏁 Script executed:
git log --oneline .github/workflows/cd-staging.yml | head -20Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 110
🏁 Script executed:
git show 23c9685:.github/workflows/cd-staging.yml | head -50Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1354
🏁 Script executed:
git log --all --oneline -- .github/workflows/cd-staging.yml | catRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 146
🏁 Script executed:
git show ef56025:.github/workflows/cd-staging.yml 2>/dev/null | head -50Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1354
🏁 Script executed:
git log --all --reverse --oneline -- .github/workflows/cd-staging.ymlRepository: ProjectTech4DevAI/kaapi-backend
Length of output: 146
🏁 Script executed:
git show ef56025 --stat | head -20Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 475
🏁 Script executed:
git log --all --reverse --oneline | head -5Repository: 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.
| 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.
| logger.info( | ||
| f"[poll_batch_status] Updated | id={batch_job.id} | " | ||
| f"{batch_job.provider_status} -> {provider_status}" | ||
| ) |
There was a problem hiding this comment.
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.
| # 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, | ||
| } | ||
| ] | ||
|
|
There was a problem hiding this comment.
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.
| 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}", | ||
| ) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| @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 | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
|
Had to close this PR because recent merges to main caused many merge conflict because of which this PR became really messy |
Summary
Target issue is #489
Checklist
Before submitting a pull request, please ensure that you mark these task.
fastapi run --reload app/main.pyordocker compose upin the repository root and test.Notes
Please add here if any other information is required for the reviewer.
Summary by CodeRabbit
New Features
Refactor
Tests
✏️ Tip: You can customize this high-level summary in your review settings.