Files

123 lines
4.9 KiB
Python
Raw Permalink Normal View History

2026-01-03 10:23:05 -06:00
from google import genai
from google.genai import types
import os
import time
from .note_server import NoteServer
class ObsidianAgent:
def __init__(self, api_key: str, server: NoteServer):
self.client = genai.Client(api_key=api_key)
self.server = server
self.model_id = "gemini-3-flash-preview"
def process_vault(self, input_folder: str, philosophy: str, rewrite_tag: str) -> list[str]:
"""
Scans the input folder and processes each note using the LLM.
Returns a list of action logs.
"""
logs = []
notes = self.server.list_notes(input_folder)
# Derive Vault Root (Assuming input_folder is inside the vault, e.g. .../Vault/Inbox)
# We take the parent directory of the input folder.
vault_root = os.path.dirname(input_folder.rstrip(os.sep))
if not notes:
return ["No notes found in input folder."]
logs.append(f"Found {len(notes)} notes. Processing (Slow mode for Rate Limits)...")
for i, note_path in enumerate(notes):
# RATE LIMITING: Sleep to respect 5 RPM (1 request every 12s + buffer)
if i > 0:
time.sleep(15)
content = self.server.read_note(note_path)
if not content:
continue
log = self._process_single_note_with_retry(note_path, content, philosophy, rewrite_tag, vault_root)
logs.append(log)
return logs
def _process_single_note_with_retry(self, note_path: str, content: str, philosophy: str, rewrite_tag: str, vault_root: str) -> str:
"""Wraps processing with retry logic for 429 errors."""
max_retries = 3
for attempt in range(max_retries):
try:
return self._process_single_note(note_path, content, philosophy, rewrite_tag, vault_root)
except Exception as e:
if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e):
if attempt < max_retries - 1:
wait_time = 60 * (attempt + 1)
print(f"Rate limit hit. Waiting {wait_time}s...")
time.sleep(wait_time)
continue
return f"Error processing {os.path.basename(note_path)}: {str(e)}"
return f"Failed after retries: {os.path.basename(note_path)}"
def _process_single_note(self, note_path: str, content: str, philosophy: str, rewrite_tag: str, vault_root: str) -> str:
# 1. Define Tools
def move_note(target_folder: str):
"""Moves the note to a folder relative to Vault Root. E.g. 'Science/Biology'."""
# Ensure we don't treat target_folder as absolute unless it starts with vault_root
if not target_folder.startswith(vault_root):
full_target = os.path.join(vault_root, target_folder.strip(os.sep))
else:
full_target = target_folder
return self.server.move_note(note_path, full_target)
def flag_rewrite(reason: str):
"""Flags the current note for rewrite by appending a tag and reason."""
return self.server.flag_rewrite(note_path, reason, rewrite_tag)
# 2. Construct Prompt
prompt = f"""
You are an expert Knowledge Manager for an Obsidian Vault.
PHILOSOPHY:
{philosophy}
CURRENT NOTE CONTENT:
{content[:10000]} # Truncate to avoid massive context if note is huge
TASK:
Analyze the note content against the Philosophy.
1. If it fits well into a specific folder in the vault structure, move it there.
2. If it is low quality, incomplete, or violates the philosophy, flag it for rewrite.
You MUST call a tool.
"""
# 3. Call Gemini
response = self.client.models.generate_content(
model=self.model_id,
contents=prompt,
config=types.GenerateContentConfig(
tools=[move_note, flag_rewrite],
tool_config=types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(
mode="ANY" # Force the model to use a tool
)
)
)
)
# 4. Execute Tool
if not response.function_calls:
return f"Skipped: {os.path.basename(note_path)} (Model failed to call tool)"
# Execute the first function call found
fc = response.function_calls[0]
if fc.name == "move_note":
result = move_note(**fc.args)
return f"[AI] {result}"
elif fc.name == "flag_rewrite":
result = flag_rewrite(**fc.args)
return f"[AI] {result}"
else:
return f"Error: Unknown tool called {fc.name}"