Skip to content

Commit 1fc36ef

Browse files
authored
Merge pull request #61 from GunaPalanivel/feature/19-file-upload-support
Add file upload support for chat sessions (#19)
2 parents 7d7b9a7 + d163334 commit 1fc36ef

File tree

17 files changed

+2114
-43
lines changed

17 files changed

+2114
-43
lines changed

chatbot-core/api/models/schemas.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,30 @@
66
"""
77

88
from enum import Enum
9-
from pydantic import BaseModel, field_validator
9+
from typing import List, Optional
10+
from pydantic import BaseModel, field_validator, model_validator
11+
12+
13+
class FileType(str, Enum):
14+
"""Enum representing supported file types."""
15+
TEXT = "text"
16+
IMAGE = "image"
17+
18+
19+
class FileAttachment(BaseModel):
20+
"""
21+
Represents a processed file attachment.
22+
23+
Fields:
24+
filename (str): Original name of the uploaded file.
25+
type (FileType): Type of file - TEXT or IMAGE.
26+
content (str): Text content or base64 encoded image data.
27+
mime_type (str): MIME type of the file.
28+
"""
29+
filename: str
30+
type: FileType
31+
content: str
32+
mime_type: str
1033

1134

1235
class ChatRequest(BaseModel):
@@ -28,12 +51,80 @@ def message_must_not_be_empty(cls, v): # pylint: disable=no-self-argument
2851
raise ValueError("Message cannot be empty.")
2952
return v
3053

54+
55+
class ChatRequestWithFiles(BaseModel):
56+
"""
57+
Represents a user message with optional file attachments.
58+
59+
Fields:
60+
message (str): The user's input message.
61+
files (List[FileAttachment]): Optional list of file attachments.
62+
63+
Validation:
64+
- Rejects when both message is empty and no files are attached.
65+
"""
66+
message: str = ""
67+
files: Optional[List[FileAttachment]] = None
68+
69+
@model_validator(mode="after")
70+
def validate_message_or_files(self):
71+
"""Validates that at least message or files are present."""
72+
has_message = bool(self.message and self.message.strip())
73+
has_files = bool(self.files and len(self.files) > 0)
74+
if not has_message and not has_files:
75+
raise ValueError("Either message or files must be provided.")
76+
return self
77+
3178
class ChatResponse(BaseModel):
3279
"""
3380
Represents the chatbot's reply.
3481
"""
3582
reply: str
3683

84+
85+
class ChatResponseWithFiles(BaseModel):
86+
"""
87+
Represents the chatbot's reply with information about processed files.
88+
89+
Fields:
90+
reply (str): The chatbot's text response.
91+
processed_files (List[str]): List of filenames that were processed.
92+
"""
93+
reply: str
94+
processed_files: Optional[List[str]] = None
95+
96+
97+
class FileUploadResponse(BaseModel):
98+
"""
99+
Response model for file upload operations.
100+
101+
Fields:
102+
success (bool): Whether the upload was successful.
103+
filename (str): Name of the uploaded file.
104+
type (str): Type of file processed ("text" or "image").
105+
message (str): Status message.
106+
"""
107+
success: bool
108+
filename: str
109+
type: str
110+
message: str
111+
112+
113+
class SupportedExtensionsResponse(BaseModel):
114+
"""
115+
Response model for supported file extensions.
116+
117+
Fields:
118+
text (List[str]): List of supported text file extensions.
119+
image (List[str]): List of supported image file extensions.
120+
max_text_size_mb (float): Maximum text file size in MB.
121+
max_image_size_mb (float): Maximum image file size in MB.
122+
"""
123+
text: List[str]
124+
image: List[str]
125+
max_text_size_mb: float
126+
max_image_size_mb: float
127+
37128
class SessionResponse(BaseModel):
38129
"""
39130
Response model when a new chat session is created.

chatbot-core/api/routes/chatbot.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@
66
the chat service logic.
77
"""
88

9-
from fastapi import APIRouter, HTTPException, Response, status
9+
from typing import List, Optional
10+
from fastapi import APIRouter, HTTPException, Response, status, UploadFile, File, Form
1011
from api.models.schemas import (
1112
ChatRequest,
1213
ChatResponse,
1314
SessionResponse,
14-
DeleteResponse
15+
DeleteResponse,
16+
FileAttachment,
17+
SupportedExtensionsResponse
1518
)
1619
from api.services.chat_service import get_chatbot_reply
1720
from api.services.memory import (
1821
init_session,
1922
delete_session,
2023
session_exists
2124
)
25+
from api.services.file_service import (
26+
process_uploaded_file,
27+
get_supported_extensions,
28+
FileProcessingError
29+
)
2230

2331
router = APIRouter()
2432

@@ -61,6 +69,90 @@ def chatbot_reply(session_id: str, request: ChatRequest):
6169
return get_chatbot_reply(session_id, request.message)
6270

6371

72+
@router.post("/sessions/{session_id}/message/upload", response_model=ChatResponse)
73+
async def chatbot_reply_with_files(
74+
session_id: str,
75+
message: str = Form(...),
76+
files: Optional[List[UploadFile]] = File(None)
77+
):
78+
"""
79+
POST endpoint to handle chatbot replies with file uploads.
80+
81+
Receives a user message with optional file attachments and returns
82+
the assistant's reply. Files are processed and their content is
83+
included in the context for the LLM.
84+
85+
Supported file types:
86+
- Text files: .txt, .log, .md, .json, .xml, .yaml, .yml, code files
87+
- Image files: .png, .jpg, .jpeg, .gif, .webp, .bmp
88+
89+
Args:
90+
session_id (str): The ID of the session from the URL path.
91+
message (str): The user's message (form field).
92+
files (List[UploadFile]): Optional list of uploaded files.
93+
94+
Returns:
95+
ChatResponse: The chatbot's generated reply.
96+
97+
Raises:
98+
HTTPException: 404 if session not found, 400 if file processing fails,
99+
422 if message is empty and no files provided.
100+
"""
101+
if not session_exists(session_id):
102+
raise HTTPException(status_code=404, detail="Session not found.")
103+
104+
# Validate that at least message or files are provided
105+
has_message = message and message.strip()
106+
has_files = files and len(files) > 0
107+
108+
if not has_message and not has_files:
109+
raise HTTPException(
110+
status_code=422,
111+
detail="Either message or files must be provided."
112+
)
113+
114+
# Process uploaded files
115+
processed_files: List[FileAttachment] = []
116+
117+
if files:
118+
for upload_file in files:
119+
try:
120+
content = await upload_file.read()
121+
processed = process_uploaded_file(
122+
content, upload_file.filename or "unknown"
123+
)
124+
processed_files.append(FileAttachment(**processed))
125+
except FileProcessingError as e:
126+
raise HTTPException(status_code=400, detail=str(e)) from e
127+
except Exception as e:
128+
raise HTTPException(
129+
status_code=500,
130+
detail=f"Failed to process file: {type(e).__name__}"
131+
) from e
132+
finally:
133+
await upload_file.close()
134+
135+
# Use default message if only files provided
136+
final_message = message.strip() if has_message else "Please analyze the attached file(s)."
137+
138+
return get_chatbot_reply(
139+
session_id, final_message, processed_files if processed_files else None
140+
)
141+
142+
143+
@router.get("/files/supported-extensions", response_model=SupportedExtensionsResponse)
144+
def get_supported_file_extensions():
145+
"""
146+
GET endpoint to retrieve supported file extensions for upload.
147+
148+
Returns:
149+
SupportedExtensionsResponse: Lists of supported text and image extensions,
150+
along with size limits.
151+
"""
152+
extensions = get_supported_extensions()
153+
return SupportedExtensionsResponse(**extensions)
154+
155+
64156
@router.delete("/sessions/{session_id}", response_model=DeleteResponse)
65157
def delete_chat(session_id: str):
66158
"""

chatbot-core/api/services/chat_service.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
RETRIEVER_AGENT_PROMPT,
1616
CONTEXT_RELEVANCE_PROMPT
1717
)
18-
from api.models.schemas import ChatResponse, QueryType, try_str_to_query_type
18+
from api.models.schemas import ChatResponse, QueryType, try_str_to_query_type, FileAttachment
1919
from api.services.memory import get_session
20+
from api.services.file_service import format_file_context
2021
from api.models.embedding_model import EMBEDDING_MODEL
2122
from api.tools.tools import TOOL_REGISTRY
2223
from api.tools.utils import get_default_tools_call, validate_tool_calls, make_placeholder_replacer
@@ -28,34 +29,57 @@
2829
retrieval_config = CONFIG["retrieval"]
2930
CODE_BLOCK_PLACEHOLDER_PATTERN = r"\[\[(?:CODE_BLOCK|CODE_SNIPPET)_(\d+)\]\]"
3031

31-
def get_chatbot_reply(session_id: str, user_input: str) -> ChatResponse:
32+
def get_chatbot_reply(
33+
session_id: str,
34+
user_input: str,
35+
files: Optional[List[FileAttachment]] = None
36+
) -> ChatResponse:
3237
"""
3338
Main chatbot entry point. Retrieves context, constructs a prompt with memory,
3439
and generates an LLM response. Also updates the memory with the latest exchange.
3540
3641
Args:
3742
session_id (str): The unique ID for the chat session.
3843
user_input (str): The latest user message.
44+
files (Optional[List[FileAttachment]]): Optional list of file attachments.
3945
4046
Returns:
4147
ChatResponse: The generated assistant response.
4248
"""
4349
logger.info("New message from session '%s'", session_id)
4450
logger.info("Handling the user query: %s", user_input)
4551

52+
if files:
53+
logger.info("Processing %d uploaded file(s)", len(files))
54+
4655
memory = get_session(session_id)
4756
if memory is None:
4857
raise RuntimeError(f"Session '{session_id}' not found in the memory store.")
4958

5059
context = retrieve_context(user_input)
5160
logger.info("Context retrieved: %s", context)
5261

62+
# Add file context if files are provided
63+
file_context = ""
64+
if files:
65+
file_dicts = [file.model_dump() for file in files]
66+
file_context = format_file_context(file_dicts)
67+
if file_context:
68+
logger.info("File context added: %d characters", len(file_context))
69+
context = f"{context}\n\n[User Uploaded Files]\n{file_context}"
70+
5371
prompt = build_prompt(user_input, context, memory)
5472

5573
logger.info("Generating answer with prompt: %s", prompt)
5674
reply = generate_answer(prompt)
5775

58-
memory.chat_memory.add_user_message(user_input)
76+
# Include file info in memory message
77+
user_message = user_input
78+
if files:
79+
file_names = [f.filename for f in files]
80+
user_message = f"{user_input}\n[Attached files: {', '.join(file_names)}]"
81+
82+
memory.chat_memory.add_user_message(user_message)
5983
memory.chat_memory.add_ai_message(reply)
6084

6185
return ChatResponse(reply=reply)

0 commit comments

Comments
 (0)