Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions backend/app/alembic/versions/045_add_llm_chain_job_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""add LLM_CHAIN job type
Revision ID: 045
Revises: 044
Create Date: 2026-02-04 00:35:43.891644
"""
from alembic import op

# revision identifiers, used by Alembic.
revision = "045"
down_revision = "044"
branch_labels = None
depends_on = None


def upgrade():
op.execute("ALTER TYPE jobtype ADD VALUE IF NOT EXISTS 'LLM_CHAIN'")


def downgrade():
pass
Comment on lines +17 to +22
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the migration file
find . -name "045_add_llm_chain_job_type.py" -type f

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 135


🏁 Script executed:

# Check the file structure and content around lines 17-22
if [ -f "backend/app/alembic/versions/045_add_llm_chain_job_type.py" ]; then
  cat -n "backend/app/alembic/versions/045_add_llm_chain_job_type.py"
fi

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 591


🏁 Script executed:

# Check other migration files to see the pattern used
fd "\.py$" backend/app/alembic/versions/ | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 4173


Migration files lack type hints across the codebase.

The upgrade() and downgrade() functions should include explicit return type hints (-> None) per coding guidelines. This is a codebase-wide issue affecting all migration files (001–045), not limited to this revision. Consider adding type hints to all migration functions for consistency.

🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/045_add_llm_chain_job_type.py` around lines 17 -
22, Add explicit return type hints to the migration functions by changing the
signatures of upgrade() and downgrade() to include "-> None" (i.e., def
upgrade() -> None: and def downgrade() -> None:), and apply the same change
across all migration revision files (001–045) so every migration function
follows the codebase guideline for type hints.

3 changes: 2 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
fine_tuning,
model_evaluation,
collection_job,
llm_chain,
)
from app.api.routes.evaluations import dataset as evaluation_dataset, evaluation
from app.core.config import settings
Expand Down Expand Up @@ -51,7 +52,7 @@
api_router.include_router(utils.router)
api_router.include_router(fine_tuning.router)
api_router.include_router(model_evaluation.router)

api_router.include_router(llm_chain.router)

if settings.ENVIRONMENT in ["development", "testing"]:
api_router.include_router(private.router)
41 changes: 41 additions & 0 deletions backend/app/api/routes/llm_chain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging

from fastapi import APIRouter, Depends

from app.api.deps import AuthContextDep, SessionDep
from app.api.permissions import Permission, require_permission
from app.models import LLMChainRequest, Message
from app.services.llm.chain_executor import start_chain_job
from app.utils import APIResponse, validate_callback_url

logger = logging.getLogger(__name__)

router = APIRouter(tags=["llm"])


@router.post(
"/llm/chain",
response_model=APIResponse[Message],
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def llm_chain(
_current_user: AuthContextDep, _session: SessionDep, request: LLMChainRequest
):
Comment on lines +21 to +23
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "llm_chain.py" -type f | grep -E "backend/app/api"

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 112


🏁 Script executed:

head -50 backend/app/api/routes/llm_chain.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1231


🏁 Script executed:

wc -l backend/app/api/routes/llm_chain.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 113


Add a return type hint for llm_chain.

The function is missing a return type annotation. Since the decorator specifies response_model=APIResponse[Message] and the function returns an APIResponse object, add the corresponding return type:

-def llm_chain(
-    _current_user: AuthContextDep, _session: SessionDep, request: LLMChainRequest
-):
+def llm_chain(
+    _current_user: AuthContextDep, _session: SessionDep, request: LLMChainRequest
+) -> APIResponse[Message]:

As per coding guidelines: Always add type hints to all function parameters and return values in Python code.

🤖 Prompt for AI Agents
In `@backend/app/api/routes/llm_chain.py` around lines 21 - 23, The function
llm_chain is missing a return type annotation; update its signature to include
the appropriate return type matching the decorator (i.e., APIResponse[Message])
so the function signature for llm_chain explicitly returns APIResponse[Message];
locate the llm_chain definition and add the return type hint to match the
response_model.

project_id = _current_user.project_.id
organization_id = _current_user.organization_.id

if request.callback_url:
validate_callback_url(str(request.callback_url))

start_chain_job(
db=_session,
request=request,
project_id=project_id,
organization_id=organization_id,
)

return APIResponse.success_response(
data=Message(
message="Chain execution started. Results will be delivered via callback."
)
)
3 changes: 3 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@
ConfigBlob,
CompletionConfig,
LLMCallRequest,
LLMChainRequest,
LLMCallResponse,
LLMChainResponse,
LlmCall,
)

from .message import Message
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class JobStatus(str, Enum):
class JobType(str, Enum):
RESPONSE = "RESPONSE"
LLM_API = "LLM_API"
LLM_CHAIN = "LLM_CHAIN"


class Job(SQLModel, table=True):
Expand Down
10 changes: 9 additions & 1 deletion backend/app/models/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@
KaapiLLMParams,
KaapiCompletionConfig,
NativeCompletionConfig,
LlmCall,
LLMChainRequest,
)
from app.models.llm.response import (
LLMCallResponse,
LLMResponse,
LLMOutput,
Usage,
LLMChainResponse,
)
from app.models.llm.response import LLMCallResponse, LLMResponse, LLMOutput, Usage
216 changes: 216 additions & 0 deletions backend/app/models/llm/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from sqlmodel import Field, SQLModel
from pydantic import Discriminator, model_validator, HttpUrl

from typing import Dict


class KaapiLLMParams(SQLModel):
"""
Expand Down Expand Up @@ -120,10 +122,17 @@ class KaapiCompletionConfig(SQLModel):
]


class PromptTemplateConfig(SQLModel):
template: str = Field(..., description="prompt template")


class ConfigBlob(SQLModel):
"""Raw JSON blob of config."""

completion: CompletionConfig = Field(..., description="Completion configuration")
prompt_template: PromptTemplateConfig | None = Field(
default=None, description="optional prompt template"
)
# Future additions:
# classifier: ClassifierConfig | None = None
# pre_filter: PreFilterConfig | None = None
Expand Down Expand Up @@ -223,3 +232,210 @@ class LLMCallRequest(SQLModel):
"The exact dictionary provided here will be returned in the response metadata field."
),
)


class ChainBlock(SQLModel):
config: LLMCallConfig = Field(
..., description="LLM call configuration for this block"
)
intermediate_callback: bool = Field(
default=False,
description="Optional callback URL for intermediary results after this block completes",
)
include_provider_raw_response: bool = Field(
default=False,
description="Whether to include the raw LLM provider response in the output",
)

request_metadata: dict[str, Any] | None = Field(
default=None,
description=(
"Client-provided metadata passed through unchanged in the response. "
"Use this to correlate responses with requests or track request state. "
"The exact dictionary provided here will be returned in the response metadata field."
),
)


class LLMChainRequest(SQLModel):
# query
query: QueryParams = Field(..., description="Query-specific parameters")

# blocks
blocks: list[ChainBlock] = Field(
..., min_length=1, description="Ordered list of blocks to execute"
)

# callback_url
callback_url: HttpUrl | None = Field(
default=None, description="Webhook URL for async response delivery"
)


class LlmCall(SQLModel, table=True):
"""
Database model for tracking LLM API call requests and responses.

Stores both request inputs and response outputs for traceability,
supporting multimodal inputs (text, audio, image) and various completion types.
"""

__tablename__ = "llm_call"
__table_args__ = (
Index(
"idx_llm_call_job_id",
"job_id",
postgresql_where=text("deleted_at IS NULL"),
),
Index(
"idx_llm_call_conversation_id",
"conversation_id",
postgresql_where=text("conversation_id IS NOT NULL AND deleted_at IS NULL"),
),
)
Comment on lines +275 to +295
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing imports cause NameError at runtime.

The LlmCall model references Index, text, uuid4, sa, JSONB, datetime, and now without importing them. This causes the pipeline failure and will prevent the module from loading.

🐛 Proposed fix: Add missing imports at the top of the file
 from typing import Annotated, Any, Literal, Union
 
 from uuid import UUID
+from uuid import uuid4
+from datetime import datetime
+
+import sqlalchemy as sa
+from sqlalchemy import Index, text
+from sqlalchemy.dialects.postgresql import JSONB
 from sqlmodel import Field, SQLModel
 from pydantic import Discriminator, model_validator, HttpUrl
 
-from typing import Dict
+from app.core.util import now
📝 Committable suggestion

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

Suggested change
class LlmCall(SQLModel, table=True):
"""
Database model for tracking LLM API call requests and responses.
Stores both request inputs and response outputs for traceability,
supporting multimodal inputs (text, audio, image) and various completion types.
"""
__tablename__ = "llm_call"
__table_args__ = (
Index(
"idx_llm_call_job_id",
"job_id",
postgresql_where=text("deleted_at IS NULL"),
),
Index(
"idx_llm_call_conversation_id",
"conversation_id",
postgresql_where=text("conversation_id IS NOT NULL AND deleted_at IS NULL"),
),
)
from typing import Annotated, Any, Literal, Union
from uuid import UUID
from uuid import uuid4
from datetime import datetime
import sqlalchemy as sa
from sqlalchemy import Index, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, SQLModel
from pydantic import Discriminator, model_validator, HttpUrl
from app.core.util import now
🧰 Tools
🪛 GitHub Actions: Kaapi CI

[error] 285-285: NameError: name 'Index' is not defined

🪛 Ruff (0.14.14)

[error] 285-285: Undefined name Index

(F821)


[error] 288-288: Undefined name text

(F821)


[error] 290-290: Undefined name Index

(F821)


[error] 293-293: Undefined name text

(F821)

🤖 Prompt for AI Agents
In `@backend/app/models/llm/request.py` around lines 275 - 295, The LlmCall model
uses several names that aren’t imported, causing NameError; add appropriate
imports at the top of the file for Index and text (from sqlalchemy), uuid4 (from
uuid), sa (import sqlalchemy as sa), JSONB (from sqlalchemy.dialects.postgresql
import JSONB), and datetime/now utilities (e.g., from datetime import datetime
and/or from sqlalchemy import func as now or use sa.func.now) so symbols
referenced in LlmCall (Index, text, uuid4, sa, JSONB, datetime, now) are defined
and the module can import cleanly.


id: UUID = Field(
default_factory=uuid4,
primary_key=True,
sa_column_kwargs={"comment": "Unique identifier for the LLM call record"},
)

job_id: UUID = Field(
foreign_key="job.id",
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={
"comment": "Reference to the parent job (status tracked in job table)"
},
)

project_id: int = Field(
foreign_key="project.id",
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={
"comment": "Reference to the project this LLM call belongs to"
},
)

organization_id: int = Field(
foreign_key="organization.id",
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={
"comment": "Reference to the organization this LLM call belongs to"
},
)

# Request fields
input: str = Field(
...,
sa_column_kwargs={
"comment": "User input - text string, binary data, or file path for multimodal"
},
)

input_type: Literal["text", "audio", "image"] = Field(
...,
sa_column=sa.Column(
sa.String,
nullable=False,
comment="Input type: text, audio, image",
),
)

output_type: Literal["text", "audio", "image"] | None = Field(
default=None,
sa_column=sa.Column(
sa.String,
nullable=True,
comment="Expected output type: text, audio, image",
),
)

# Provider and model info
provider: str = Field(
...,
sa_column=sa.Column(
sa.String,
nullable=False,
comment="AI provider as sent by user (e.g openai, -native, google)",
),
)

model: str = Field(
...,
sa_column_kwargs={
"comment": "Specific model used e.g. 'gpt-4o', 'gemini-2.5-pro'"
},
)

# Response fields
provider_response_id: str | None = Field(
default=None,
sa_column_kwargs={
"comment": "Original response ID from the provider (e.g., OpenAI's response ID)"
},
)

content: dict[str, Any] | None = Field(
default=None,
sa_column=sa.Column(
JSONB,
nullable=True,
comment="Response content: {text: '...'}, {audio_bytes: '...'}, or {image: '...'}",
),
)

usage: dict[str, Any] | None = Field(
default=None,
sa_column=sa.Column(
JSONB,
nullable=True,
comment="Token usage: {input_tokens, output_tokens, reasoning_tokens}",
),
)

# Conversation tracking
conversation_id: str | None = Field(
default=None,
sa_column_kwargs={
"comment": "Identifier linking this response to its conversation thread"
},
)

auto_create: bool | None = Field(
default=None,
sa_column_kwargs={
"comment": "Whether to auto-create conversation if conversation_id doesn't exist (OpenAI specific)"
},
)

# Configuration - stores either {config_id, config_version} or {config_blob}
config: dict[str, Any] | None = Field(
default=None,
sa_column=sa.Column(
JSONB,
nullable=True,
comment="Configuration: {config_id, config_version} for stored config OR {config_blob} for ad-hoc config",
),
)

# Timestamps
created_at: datetime = Field(
default_factory=now,
nullable=False,
sa_column_kwargs={"comment": "Timestamp when the LLM call was created"},
)

updated_at: datetime = Field(
default_factory=now,
nullable=False,
sa_column_kwargs={"comment": "Timestamp when the LLM call was last updated"},
)

deleted_at: datetime | None = Field(
default=None,
nullable=True,
sa_column_kwargs={"comment": "Timestamp when the record was soft-deleted"},
)
7 changes: 7 additions & 0 deletions backend/app/models/llm/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ class LLMCallResponse(SQLModel):
default=None,
description="Unmodified raw response from the LLM provider.",
)


class LLMChainResponse(SQLModel):
response: LLMCallResponse = Field(
..., description="Full response from the last block in the chain"
)
# blocks_executed: int = Field(..., description="Total number of blocks executed")
Loading
Loading