Skip to content
Merged
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
4 changes: 3 additions & 1 deletion backend/api/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ async def get_profile(request: Request) -> UserInfo:
"""

if not settings.LDAP_ENABLED:
return UserInfo(username="anonymous", display_name="anonymous", email=None)
return UserInfo(
username="anonymous", display_name="anonymous", email=None, is_admin=True
)

user = get_current_user(request)
username = user.get("sub", user.get("username", ""))
Expand Down
79 changes: 79 additions & 0 deletions backend/api/api_collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Author: Charm
Copyright (c) 2025, All Rights Reserved.
"""

from typing import Any, Dict, Optional

from fastapi import APIRouter, Query, Request

from model.collection import (
CollectionCreateRequest,
CollectionTaskAddRequest,
CollectionUpdateRequest,
)
from service.collection_service import (
add_task_to_collection_svc,
create_collection_svc,
delete_collection_svc,
get_collection_svc,
list_collection_tasks_svc,
list_collections_svc,
remove_task_from_collection_svc,
update_collection_svc,
)

router = APIRouter()


@router.post("", response_model=Dict[str, Any])
async def create_collection(
request: Request, collection_create: CollectionCreateRequest
):
return await create_collection_svc(request, collection_create)


@router.get("", response_model=Dict[str, Any])
async def list_collections(
request: Request,
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = None,
):
return await list_collections_svc(request, page, page_size, search)


@router.get("/{collection_id}", response_model=Dict[str, Any])
async def get_collection(request: Request, collection_id: str):
return await get_collection_svc(request, collection_id)


@router.put("/{collection_id}", response_model=Dict[str, Any])
async def update_collection(
request: Request, collection_id: str, collection_update: CollectionUpdateRequest
):
return await update_collection_svc(request, collection_id, collection_update)


@router.delete("/{collection_id}", response_model=Dict[str, Any])
async def delete_collection(request: Request, collection_id: str):
return await delete_collection_svc(request, collection_id)


@router.post("/{collection_id}/tasks", response_model=Dict[str, Any])
async def add_task_to_collection(
request: Request, collection_id: str, task_req: CollectionTaskAddRequest
):
return await add_task_to_collection_svc(request, collection_id, task_req)


@router.delete("/{collection_id}/tasks/{task_id}", response_model=Dict[str, Any])
async def remove_task_from_collection(
request: Request, collection_id: str, task_id: str
):
return await remove_task_from_collection_svc(request, collection_id, task_id)


@router.get("/{collection_id}/tasks", response_model=Dict[str, Any])
async def list_collection_tasks(request: Request, collection_id: str):
return await list_collection_tasks_svc(request, collection_id)
22 changes: 20 additions & 2 deletions backend/api/api_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
"""

from fastapi import APIRouter, Query
from fastapi.responses import FileResponse

from model.log import LogContentResponse
from service.log_service import get_service_log_svc, get_task_log_svc
from service.log_service import (
download_task_log_svc,
get_service_log_svc,
get_task_log_svc,
)

# Create an API router for log-related endpoints
router = APIRouter()
Expand Down Expand Up @@ -38,6 +43,7 @@ async def get_task_log(
task_id: str,
offset: int = Query(default=0),
tail: int = Query(default=0),
source: str = Query(default="engine", description="Log source: engine or backend"),
):
"""
Get the log content of a specified task.
Expand All @@ -46,9 +52,21 @@ async def get_task_log(
task_id (str): The ID of the task.
offset (int): The offset in bytes from the beginning of the log file. Ineffective when tail > 0.
tail (int): The number of lines to read from the end of the log file. When tail > 0, offset is ignored.
source (str): The source of the log ("engine" or "backend").

Returns:
LogContentResponse: An object containing the log content.
By default (offset=0, tail=0), the entire log file is read.
"""
return await get_task_log_svc(task_id, offset, tail)
return await get_task_log_svc(task_id, offset, tail, source)


@router.get("/task/{task_id}/download", response_class=FileResponse)
async def download_task_log(
task_id: str,
source: str = Query(default="engine", description="Log source: engine or backend"),
):
"""
Download the full output log for a given task.
"""
return await download_task_log_svc(task_id, source)
2 changes: 2 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from api.api_analysis import router as analysis
from api.api_auth import router as auth
from api.api_collection import router as collection
from api.api_http_task import router as http_task
from api.api_llm_task import router as llm_task
from api.api_log import router as log
Expand Down Expand Up @@ -121,6 +122,7 @@ def read_root():
# add api routers
app.include_router(analysis, prefix="/api/analyze", tags=["analysis"])
app.include_router(auth, prefix="/api/auth", tags=["auth"])
app.include_router(collection, prefix="/api/collections", tags=["collections"])
app.include_router(system, prefix="/api/system", tags=["system"])
app.include_router(llm_task, prefix="/api/llm-tasks", tags=["llm-tasks"])
app.include_router(http_task, prefix="/api/http-tasks", tags=["http-tasks"])
Expand Down
107 changes: 107 additions & 0 deletions backend/model/collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Author: Charm
Copyright (c) 2025, All Rights Reserved.
"""

from typing import List, Optional

from pydantic import BaseModel, Field
from sqlalchemy import Column, DateTime, Integer, String, Text, UniqueConstraint
from sqlalchemy.sql import func

from db.mysql import Base


class Collection(Base):
"""
SQLAlchemy model representing a collection in the 'collections' table.
"""

__tablename__ = "collections"

id = Column(String(40), primary_key=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
rich_content = Column(Text, nullable=True)
created_by = Column(String(100), nullable=True)
is_public = Column(Integer, nullable=False, default=1, server_default="1")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())


class CollectionTask(Base):
"""
SQLAlchemy model representing the many-to-many relationship between collections and tasks.
"""

__tablename__ = "collection_tasks"

id = Column(Integer, primary_key=True, autoincrement=True)
collection_id = Column(String(40), nullable=False, index=True)
task_id = Column(String(40), nullable=False, index=True)
task_type = Column(String(16), nullable=False)
created_at = Column(DateTime, server_default=func.now())

__table_args__ = (
UniqueConstraint("collection_id", "task_id", name="uk_collection_task"),
)


# Pydantic models for API
class CollectionCreateRequest(BaseModel):
"""Payload for creating a collection."""

name: str = Field(..., description="Collection name")
description: Optional[str] = Field(None, description="Collection description")
rich_content: Optional[str] = Field(None, description="Rich text content")
is_public: Optional[bool] = Field(
True, description="Whether the collection is public"
)


class CollectionUpdateRequest(BaseModel):
"""Payload for partially updating a collection."""

name: Optional[str] = Field(None, description="Collection name")
description: Optional[str] = Field(None, description="Collection description")
rich_content: Optional[str] = Field(None, description="Rich text content")
is_public: Optional[bool] = Field(
None, description="Whether the collection is public"
)


class CollectionTaskAddRequest(BaseModel):
"""Payload for adding a task into a collection."""

task_id: str = Field(..., description="Task ID")
task_type: str = Field(..., description="Task type: http or llm")


class CollectionTaskRemoveRequest(BaseModel):
"""Payload for removing a task from a collection."""

task_id: str = Field(..., description="Task ID")


class CollectionTaskResponse(BaseModel):
"""Response schema for a collection-task relation row."""

id: int
collection_id: str
task_id: str
task_type: str
created_at: str


class CollectionResponse(BaseModel):
"""Response schema for collection detail/list items."""

id: str
name: str
description: Optional[str]
rich_content: Optional[str]
created_by: Optional[str]
is_public: bool
created_at: str
updated_at: str
task_count: int = 0
2 changes: 2 additions & 0 deletions backend/model/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class LogContentResponse(BaseModel):
Attributes:
content: The content of the log file as a string.
file_size: The size of the log file in bytes.
log_url: Optional URL to download the full log file.
"""

content: str
file_size: int
log_url: str | None = None
Loading