Upload current progress
This commit is contained in:
122
obsidian_automator/agent.py
Normal file
122
obsidian_automator/agent.py
Normal file
@@ -0,0 +1,122 @@
|
||||
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}"
|
||||
Reference in New Issue
Block a user