Skip to content

Commit 10b83cb

Browse files
joadoumiesrdasdlqqq
authored
Add file attachment directly to JupyternautPersona when file is included in message (#1419)
* feat: Add file attachment processing to JupyternautPersona - Add _process_attachments() method to read and include file content in chat context - Add _resolve_attachment_to_path() to resolve attachment IDs to file paths - Add _get_attachment_from_ychat() to retrieve attachment data from YDoc This enables the Jupyternaut AI assistant to process file attachments in chat messages and include their content as context for better responses. * moved new methods to the base class in base_persona.py and cleaned up code for automated Typing test * removed get_attachment method in base persona and use the get_attachments() from ychat * use Optional type for Py39 compat --------- Co-authored-by: Sanjiv Das <[email protected]> Co-authored-by: David L. Qiu <[email protected]>
1 parent 348fb92 commit 10b83cb

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

packages/jupyter-ai/jupyter_ai/personas/base_persona.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import os
23
from abc import ABC, ABCMeta, abstractmethod
34
from dataclasses import asdict
45
from logging import Logger
@@ -346,7 +347,76 @@ def get_mcp_config(self) -> dict[str, Any]:
346347
Returns the MCP config for the current chat.
347348
"""
348349
return self.parent.get_mcp_config()
349-
350+
351+
def process_attachments(self, message: Message) -> Optional[str]:
352+
"""
353+
Process file attachments in the message and return their content as a string.
354+
"""
355+
356+
if not message.attachments:
357+
return None
358+
359+
context_parts = []
360+
361+
for attachment_id in message.attachments:
362+
self.log.info(f"FILE: Processing attachment with ID: {attachment_id}")
363+
try:
364+
# Try to resolve attachment using multiple strategies
365+
file_path = self.resolve_attachment_to_path(attachment_id)
366+
367+
if not file_path:
368+
self.log.warning(f"Could not resolve attachment ID: {attachment_id}")
369+
continue
370+
371+
# Read the file content
372+
with open(file_path, "r", encoding="utf-8") as f:
373+
file_content = f.read()
374+
375+
# Get relative path for display
376+
rel_path = os.path.relpath(file_path, self.get_workspace_dir())
377+
378+
# Add file content with header
379+
context_parts.append(
380+
f"File: {rel_path}\n```\n{file_content}\n```"
381+
)
382+
383+
except Exception as e:
384+
self.log.warning(f"Failed to read attachment {attachment_id}: {e}")
385+
context_parts.append(
386+
f"Attachment: {attachment_id} (could not read file: {e})"
387+
)
388+
389+
result = "\n\n".join(context_parts) if context_parts else None
390+
return result
391+
392+
def resolve_attachment_to_path(self, attachment_id: str) -> Optional[str]:
393+
"""
394+
Resolve an attachment ID to its file path using multiple strategies.
395+
"""
396+
397+
try:
398+
attachment_data = self.ychat.get_attachments().get(attachment_id)
399+
400+
if attachment_data and isinstance(attachment_data, dict):
401+
# If attachment has a 'value' field with filename
402+
if 'value' in attachment_data:
403+
filename = attachment_data['value']
404+
405+
# Try relative to workspace directory
406+
workspace_path = os.path.join(self.get_workspace_dir(), filename)
407+
if os.path.exists(workspace_path):
408+
return workspace_path
409+
410+
# Try as absolute path
411+
if os.path.exists(filename):
412+
return filename
413+
414+
return None
415+
416+
except Exception as e:
417+
self.log.error(f"Failed to resolve attachment {attachment_id}: {e}")
418+
return None
419+
350420
def shutdown(self) -> None:
351421
"""
352422
Shuts the persona down. This method should:

packages/jupyter-ai/jupyter_ai/personas/jupyternaut/jupyternaut.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ async def process_message(self, message: Message) -> None:
3636
provider_name = self.config_manager.lm_provider.name
3737
model_id = self.config_manager.lm_provider_params["model_id"]
3838

39+
# Process file attachments and include their content in the context
40+
context = self.process_attachments(message)
41+
3942
runnable = self.build_runnable()
4043
variables = JupyternautVariables(
4144
input=message.body,
4245
model_id=model_id,
4346
provider_name=provider_name,
4447
persona_name=self.name,
48+
context=context,
4549
)
4650
variables_dict = variables.model_dump()
4751
reply_stream = runnable.astream(variables_dict)
@@ -60,3 +64,4 @@ def build_runnable(self) -> Any:
6064
)
6165

6266
return runnable
67+

0 commit comments

Comments
 (0)