@cryptotaxi247 / CoPilot / commits / b2136271

540 persistent data on assessments (#578)

* Add file upload functionality to data store with MinIO integration * Remove dependency restriction for file upload endpoint

taylor_socfortress committed Dec 29, 2025 at 16:16 UTC b21362716d6fc639b4cec5cb292445f9a7a77835
3 files changed +134
backend/app/data_store/data_store_operations.py
+62
@@ -115,6 +115,68 @@ async def list_case_report_template_data_store_files(bucket_name: Optional[str]
115 return objects_list
116
117
118 +async def upload_file_to_datastore(
119 + file: UploadFile,
120 + bucket_name: str,
121 + object_name: str,
122 +) -> dict:
123 + """
124 + Upload a file to MinIO data store.
125 +
126 + Args:
127 + file: The file to upload
128 + bucket_name: The name of the bucket
129 + object_name: The object path/name within the bucket
130 +
131 + Returns:
132 + dict: Upload details including object_key, file_size, file_hash, and content_type
133 + """
134 + client = await create_session()
135 +
136 + # Create bucket if it doesn't exist
137 + await create_bucket_if_not_exists(bucket_name)
138 +
139 + logger.info(f"Uploading file {file.filename} to bucket {bucket_name} as {object_name}")
140 +
141 + # Define the temporary file path
142 + temp_file_path = os.path.join(os.getcwd(), file.filename)
143 +
144 + # Save the file to the temporary location and calculate hash
145 + sha256_hash = hashlib.sha256()
146 + async with aiofiles.open(temp_file_path, "wb") as out_file:
147 + content = await file.read()
148 + await out_file.write(content)
149 + sha256_hash.update(content)
150 +
151 + file_hash = sha256_hash.hexdigest()
152 + file_size = os.path.getsize(temp_file_path)
153 +
154 + # Determine content type
155 + content_type = file.content_type or "application/octet-stream"
156 +
157 + # Upload the file to MinIO
158 + await client.fput_object(
159 + bucket_name=bucket_name,
160 + object_name=object_name,
161 + file_path=temp_file_path,
162 + content_type=content_type,
163 + )
164 +
165 + # Remove the temporary file after upload
166 + os.remove(temp_file_path)
167 +
168 + logger.info(f"Successfully uploaded {file.filename} ({file_size} bytes) to {bucket_name}/{object_name}")
169 +
170 + return {
171 + "bucket_name": bucket_name,
172 + "object_key": object_name,
173 + "file_name": file.filename,
174 + "file_size": file_size,
175 + "file_hash": file_hash,
176 + "content_type": content_type,
177 + }
178 +
179 +
180 async def create_buckets() -> None:
181 await create_bucket_if_not_exists("copilot-cases")
182
backend/app/data_store/data_store_routes.py
+61
@@ -3,7 +3,10 @@ from typing import Optional
3
4 from fastapi import APIRouter
5 from fastapi import Depends
6 +from fastapi import File
7 +from fastapi import Form
8 from fastapi import HTTPException
9 +from fastapi import UploadFile
10 from fastapi import status
11 from fastapi.responses import StreamingResponse
12 from loguru import logger
@@ -15,15 +18,73 @@ from sqlalchemy.orm import selectinload
18 from app.auth.utils import AuthHandler
19 from app.data_store.data_store_operations import delete_agent_artifact_file
20 from app.data_store.data_store_operations import download_agent_artifact_file
21 +from app.data_store.data_store_operations import upload_file_to_datastore
22 from app.data_store.data_store_schema import AgentDataStoreData
23 from app.data_store.data_store_schema import AgentDataStoreListResponse
24 from app.data_store.data_store_schema import AgentDataStoreResponse
25 +from app.data_store.data_store_schema import FileUploadResponse
26 from app.db.db_session import get_db
27 from app.db.universal_models import AgentDataStore
28
29 agent_data_store_router = APIRouter()
30
31
32 +@agent_data_store_router.post(
33 + "/upload",
34 + response_model=FileUploadResponse,
35 + description="Upload a file to the data store",
36 +)
37 +async def upload_file(
38 + file: UploadFile = File(...),
39 + bucket_name: str = Form(...),
40 + object_name: str = Form(...),
41 +) -> FileUploadResponse:
42 + """
43 + Upload a file to the specified bucket and object path in the data store.
44 +
45 + Args:
46 + file: The file to upload
47 + bucket_name: The name of the bucket to upload to
48 + object_name: The object path/name within the bucket (e.g., "folder/subfolder/file.txt")
49 +
50 + Returns:
51 + FileUploadResponse with upload details
52 + """
53 + try:
54 + logger.info(f"Uploading file {file.filename} to bucket {bucket_name} as {object_name}")
55 +
56 + if not file.filename:
57 + raise HTTPException(
58 + status_code=status.HTTP_400_BAD_REQUEST,
59 + detail="File name is required",
60 + )
61 +
62 + # Upload the file to MinIO
63 + upload_result = await upload_file_to_datastore(
64 + file=file,
65 + bucket_name=bucket_name,
66 + object_name=object_name,
67 + )
68 +
69 + return FileUploadResponse(
70 + success=True,
71 + message=f"File {file.filename} uploaded successfully",
72 + bucket_name=upload_result["bucket_name"],
73 + object_key=upload_result["object_key"],
74 + file_name=upload_result["file_name"],
75 + file_size=upload_result["file_size"],
76 + file_hash=upload_result["file_hash"],
77 + content_type=upload_result.get("content_type"),
78 + )
79 +
80 + except Exception as e:
81 + logger.error(f"Failed to upload file: {e}")
82 + raise HTTPException(
83 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
84 + detail=f"Failed to upload file: {str(e)}",
85 + )
86 +
87 +
88 @agent_data_store_router.get(
89 "/agent/{agent_id}/artifacts",
90 response_model=AgentDataStoreListResponse,
backend/app/data_store/data_store_schema.py
+11
@@ -5,6 +5,17 @@ from pydantic import BaseModel
5 from pydantic import Field
6
7
8 +class FileUploadResponse(BaseModel):
9 + success: bool
10 + message: str
11 + bucket_name: str
12 + object_key: str
13 + file_name: str
14 + file_size: int
15 + file_hash: str
16 + content_type: Optional[str] = None
17 +
18 +
19 class CaseDataStoreCreation(BaseModel):
20 case_id: int
21 bucket_name: str = Field(max_length=255)