mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 13:18:18 +00:00
Add file viewer tool to enable researcher to read documents
Allow reading whole file contents or content in specified line range in user's knowledge base. This allows for more deterministic traversal.
This commit is contained in:
@@ -2801,3 +2801,80 @@ def get_notion_auth_url(user: KhojUser):
|
||||
if not NOTION_OAUTH_CLIENT_ID or not NOTION_OAUTH_CLIENT_SECRET or not NOTION_REDIRECT_URI:
|
||||
return None
|
||||
return f"https://api.notion.com/v1/oauth/authorize?client_id={NOTION_OAUTH_CLIENT_ID}&redirect_uri={NOTION_REDIRECT_URI}&response_type=code&state={user.uuid}"
|
||||
|
||||
|
||||
async def view_file_content(
|
||||
path: str,
|
||||
start_line: Optional[int] = None,
|
||||
end_line: Optional[int] = None,
|
||||
user: KhojUser = None,
|
||||
):
|
||||
"""
|
||||
View the contents of a file from the user's document database with optional line range specification.
|
||||
"""
|
||||
query = f"View file: {path}"
|
||||
if start_line and end_line:
|
||||
query += f" (lines {start_line}-{end_line})"
|
||||
|
||||
try:
|
||||
# Get the file object from the database by name
|
||||
file_objects = await FileObjectAdapters.aget_file_objects_by_name(user, path)
|
||||
|
||||
if not file_objects:
|
||||
error_msg = f"File '{path}' not found in user documents"
|
||||
logger.warning(error_msg)
|
||||
yield [{"query": query, "file": path, "compiled": error_msg}]
|
||||
return
|
||||
|
||||
# Use the first file object if multiple exist
|
||||
file_object = file_objects[0]
|
||||
raw_text = file_object.raw_text
|
||||
|
||||
# Apply line range filtering if specified
|
||||
if start_line is None and end_line is None:
|
||||
filtered_text = raw_text
|
||||
else:
|
||||
lines = raw_text.split("\n")
|
||||
start_line = start_line or 1
|
||||
end_line = end_line or len(lines)
|
||||
|
||||
# Validate line range
|
||||
if start_line < 1 or end_line < 1 or start_line > end_line:
|
||||
error_msg = f"Invalid line range: {start_line}-{end_line}"
|
||||
logger.warning(error_msg)
|
||||
yield [{"query": query, "file": path, "compiled": error_msg}]
|
||||
return
|
||||
if start_line > len(lines):
|
||||
error_msg = f"Start line {start_line} exceeds total number of lines {len(lines)}"
|
||||
logger.warning(error_msg)
|
||||
yield [{"query": query, "file": path, "compiled": error_msg}]
|
||||
return
|
||||
|
||||
# Convert from 1-based to 0-based indexing and ensure bounds
|
||||
start_idx = max(0, start_line - 1)
|
||||
end_idx = min(len(lines), end_line)
|
||||
|
||||
selected_lines = lines[start_idx:end_idx]
|
||||
filtered_text = "\n".join(selected_lines)
|
||||
|
||||
# Truncate the text if it's too long
|
||||
if len(filtered_text) > 10000:
|
||||
filtered_text = filtered_text[:10000] + "\n\n[Truncated. Use line numbers to view specific sections.]"
|
||||
|
||||
# Format the result as a document reference
|
||||
document_results = [
|
||||
{
|
||||
"query": query,
|
||||
"file": path,
|
||||
"compiled": filtered_text,
|
||||
}
|
||||
]
|
||||
|
||||
yield document_results
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error viewing file {path}: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Return an error result in the expected format
|
||||
yield [{"query": query, "file": path, "compiled": error_msg}]
|
||||
|
||||
@@ -26,6 +26,7 @@ from khoj.routers.helpers import (
|
||||
generate_summary_from_files,
|
||||
search_documents,
|
||||
send_message_to_model_wrapper,
|
||||
view_file_content,
|
||||
)
|
||||
from khoj.utils.helpers import (
|
||||
ConversationCommand,
|
||||
@@ -89,10 +90,11 @@ async def apick_next_tool(
|
||||
# Skip showing operator tool as an option if not enabled
|
||||
if tool == ConversationCommand.Operator and not is_operator_enabled():
|
||||
continue
|
||||
# Skip showing document related tools if user has no documents
|
||||
if (tool == ConversationCommand.Notes or tool == ConversationCommand.ViewFile) and not user_has_entries:
|
||||
continue
|
||||
# Skip showing Notes tool as an option if user has no entries
|
||||
elif tool == ConversationCommand.Notes:
|
||||
if not user_has_entries:
|
||||
continue
|
||||
if tool == ConversationCommand.Notes:
|
||||
description = tool_data.description.format(max_search_queries=max_document_searches)
|
||||
elif tool == ConversationCommand.Webpage:
|
||||
description = tool_data.description.format(max_webpages_to_read=max_webpages_to_read)
|
||||
@@ -426,6 +428,25 @@ async def research(
|
||||
this_iteration.warning = f"Error operating browser: {e}"
|
||||
logger.error(this_iteration.warning, exc_info=True)
|
||||
|
||||
elif this_iteration.query.name == ConversationCommand.ViewFile:
|
||||
try:
|
||||
async for result in view_file_content(
|
||||
**this_iteration.query.args,
|
||||
user=user,
|
||||
):
|
||||
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
||||
yield result[ChatEvent.STATUS]
|
||||
else:
|
||||
if this_iteration.context is None:
|
||||
this_iteration.context = []
|
||||
document_results: List[Dict[str, str]] = result # type: ignore
|
||||
this_iteration.context += document_results
|
||||
async for result in send_status_func(f"**Viewed file**: {this_iteration.query.args['path']}"):
|
||||
yield result
|
||||
except Exception as e:
|
||||
this_iteration.warning = f"Error viewing file: {e}"
|
||||
logger.error(this_iteration.warning, exc_info=True)
|
||||
|
||||
else:
|
||||
# No valid tools. This is our exit condition.
|
||||
current_iteration = MAX_ITERATIONS
|
||||
|
||||
@@ -429,6 +429,7 @@ class ConversationCommand(str, Enum):
|
||||
Diagram = "diagram"
|
||||
Research = "research"
|
||||
Operator = "operator"
|
||||
ViewFile = "view_file"
|
||||
|
||||
|
||||
command_descriptions = {
|
||||
@@ -442,6 +443,7 @@ command_descriptions = {
|
||||
ConversationCommand.Diagram: "Draw a flowchart, diagram, or any other visual representation best expressed with primitives like lines, rectangles, and text.",
|
||||
ConversationCommand.Research: "Do deep research on a topic. This will take longer than usual, but give a more detailed, comprehensive answer.",
|
||||
ConversationCommand.Operator: "Operate and perform tasks using a computer.",
|
||||
ConversationCommand.ViewFile: "View the contents of a file with optional line range specification.",
|
||||
}
|
||||
|
||||
command_descriptions_for_agent = {
|
||||
@@ -545,6 +547,35 @@ tools_for_research_llm = {
|
||||
"required": ["query"],
|
||||
},
|
||||
),
|
||||
ConversationCommand.ViewFile: ToolDefinition(
|
||||
name="view_file",
|
||||
description=dedent(
|
||||
"""
|
||||
To view the contents of specific note or document in the user's personal knowledge base.
|
||||
Especially helpful if the question expects context from the user's notes or documents.
|
||||
It can be used after finding the document path with the document search tool.
|
||||
Optionally specify a line range to view only specific sections of large files.
|
||||
"""
|
||||
).strip(),
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The file path to view (can be absolute or relative).",
|
||||
},
|
||||
"start_line": {
|
||||
"type": "integer",
|
||||
"description": "Optional starting line number for viewing a specific range (1-indexed).",
|
||||
},
|
||||
"end_line": {
|
||||
"type": "integer",
|
||||
"description": "Optional ending line number for viewing a specific range (1-indexed).",
|
||||
},
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
mode_descriptions_for_llm = {
|
||||
|
||||
Reference in New Issue
Block a user