Obsidian-AI¶
Summarize Obsidian page.
Get Python Source.
Script is written in literate programming.
Name |
URL |
|---|---|
OpenAI API Examples |
import os
import time
from dataclasses import dataclass
from pathlib import Path
import pyperclip
import streamlit as st
from PersistedList import PersistedList
Print banner in ASCII graphics
st.set_page_config(
page_title="O-AI"
)
@st.cache_data
def print_banner():
print("""
dBBBBP dBBBBBb dBP
dB'.BP BB
dB'.BP dBP BB dBP
dB'.BP dBBBBBP dBP BB dBP
dBBBBP dBBBBBBB dBP
""")
return 1
print_banner()
st.logo("https://ea-books.netlify.app/lit/ai_obsidian.svg")
Prompts and models¶
Resolve the user’s Documents folder on Windows and Unix-like systems. Remember selected prompt names separately from model names.
prompts_folder = Path.home() / "Documents" / "prompts"
prompt_files = {
path.stem: path for path in sorted(prompts_folder.glob("*.md"))
if path.is_file()
}
if not prompt_files:
st.error(f"No Markdown prompts found in {prompts_folder}.")
st.stop()
prompts_persisted = PersistedList(".o-ai-prompts")
prompt_names = prompts_persisted.sort_by_pattern(sorted(prompt_files))
prompt_name = st.sidebar.selectbox("Prompt", prompt_names)
prompts_persisted.select(prompt_name)
prompt = prompt_files[prompt_name].read_text(encoding="utf-8")
# Prices are USD per 1 million input/output tokens.
llm_prices = {
"gemini-4-argon": (2.00, 10.00),
"gemini-3.8-flash": (0.75, 3.75),
"gpt-6-luna": (0.10, 0.50),
"gpt-6.1-terra": (1.00, 5.00),
"gpt-6.1-sol": (2.00, 10.00),
}
llm_models = list(llm_prices.keys())
llm_models_persisted = PersistedList(".o-ai")
llm_models = llm_models_persisted.sort_by_pattern(llm_models)
llm_model = st.sidebar.selectbox("LLM Model", llm_models)
Obsidian notes¶
Find the first book folder in the working directory and select a recent note.
def reset_llm_result():
for key in ("llm_result", "note_name", "execution_time", "llm_cost", "llm_usage"):
st.session_state.pop(key, None)
def find_obsidian_folder():
current_folder = os.getcwd()
home_folders = os.listdir(current_folder)
book_folders = [
item for item in home_folders
if os.path.isdir(os.path.join(current_folder, item)) and item.endswith(" Book")
]
if len(book_folders) == 0:
raise ValueError('The folder should contain a subfolder with a name that ends with " Book".')
return book_folders[0]
def get_newest_files(directory, num_files):
if not os.path.isdir(directory):
raise ValueError(f"The directory {directory} does not exist.")
files = [path for path in Path(directory).iterdir() if path.is_file()]
files.sort(key=lambda path: path.stat().st_mtime, reverse=True)
return [path.name for path in files[:num_files]]
note_home = find_obsidian_folder()
newest_files = get_newest_files(note_home, 5)
if not newest_files:
st.info(f"No notes found in {note_home}.")
st.stop()
note_name = st.selectbox("Note", newest_files, on_change=reset_llm_result)
text = (Path(note_home) / note_name).read_text(encoding="utf-8")
LLM clients and pricing¶
Set OPENAI_API_KEY and GEMINI_API_KEY in the environment for the providers you use. Clients are created only when processing a note. Pre-request token counts are estimates; cost uses reported usage.
@dataclass(frozen=True)
class LLMResult:
text: str
input_tokens: int
output_tokens: int
def count_tokens(value: str) -> int:
import tiktoken
try:
encoding = tiktoken.encoding_for_model(llm_model)
except KeyError:
# General-purpose fallback; Gemini counts are estimates only.
encoding = tiktoken.get_encoding("o200k_base")
return len(encoding.encode(value))
def process_with_openai(text: str, prompt: str) -> LLMResult:
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=llm_model,
instructions=prompt.strip(),
input=text,
)
return LLMResult(
text=response.output_text,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
)
def process_with_gemini(text: str, prompt: str) -> LLMResult:
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
response = client.models.generate_content(
model=llm_model,
contents=text,
config=types.GenerateContentConfig(system_instruction=prompt.strip()),
)
usage = response.usage_metadata
# Gemini bills response and reasoning tokens at the output-token rate.
output_tokens = (usage.candidates_token_count or 0) + (
usage.thoughts_token_count or 0
)
return LLMResult(
text=response.text or "",
input_tokens=usage.prompt_token_count or 0,
output_tokens=output_tokens,
)
def process_text(text: str, prompt: str) -> LLMResult:
if llm_model.startswith("gpt-"):
return process_with_openai(text, prompt)
if llm_model.startswith("gemini-"):
return process_with_gemini(text, prompt)
raise ValueError(f"Unsupported model: {llm_model}")
OPENAI_LONG_LIMIT_TOKENS = 272 * 1024
def calculate_cost(result: LLMResult) -> float:
input_price, output_price = llm_prices[llm_model]
if llm_model.startswith("gpt-") and result.input_tokens > OPENAI_LONG_LIMIT_TOKENS:
input_price *= 2
output_price *= 1.5
return (
result.input_tokens * input_price + result.output_tokens * output_price
) / 1_000_000
estimated_tokens = count_tokens(text) + count_tokens(prompt.strip())
st.sidebar.write(f"Estimated input tokens: `{estimated_tokens:,}`")
Process and display the result¶
Store the cost with the result so changing the model does not reprice it.
def call_llm():
start_time = time.time()
st.info(prompt)
llm_models_persisted.select(llm_model)
result = process_text(text, prompt)
st.session_state.llm_result = result.text
st.session_state.llm_usage = (result.input_tokens, result.output_tokens)
st.session_state.llm_cost = calculate_cost(result)
st.session_state.note_name = note_name
st.session_state.execution_time = time.time() - start_time
pyperclip.copy(result.text)
st.rerun()
if st.button("Process", type="primary", width="stretch"):
call_llm()
if "llm_result" in st.session_state:
st.write("---")
st.write(st.session_state.llm_result)
st.write("---")
if "execution_time" in st.session_state:
st.sidebar.write(f"Execution time: `{st.session_state.execution_time:.1f}` sec")
if "llm_cost" in st.session_state:
input_tokens, output_tokens = st.session_state.llm_usage
st.sidebar.write(f"Input tokens: `{input_tokens:,}`")
st.sidebar.write(f"Output tokens: `{output_tokens:,}`")
st.sidebar.write(f"Cents: `${st.session_state.llm_cost*100:.4f}`")
if "llm_result" in st.session_state and st.sidebar.button("Copy to clipboard", width="stretch"):
pyperclip.copy(st.session_state.llm_result)