feat(tools): migrate to litellm for multi-provider support (OpenAI, Gemini, Claude)
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
anthropic>=0.40.0
|
litellm>=1.0.0
|
||||||
networkx>=3.2
|
networkx>=3.2
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import webbrowser
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import anthropic
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
@@ -64,6 +64,23 @@ def read_file(path: Path) -> str:
|
|||||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
|
||||||
|
try:
|
||||||
|
from litellm import completion
|
||||||
|
except ImportError:
|
||||||
|
print("Error: litellm not installed. Run: pip install litellm")
|
||||||
|
import sys
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = os.getenv(model_env, default_model)
|
||||||
|
response = completion(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=max_tokens
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
|
||||||
def sha256(text: str) -> str:
|
def sha256(text: str) -> str:
|
||||||
return hashlib.sha256(text.encode()).hexdigest()
|
return hashlib.sha256(text.encode()).hexdigest()
|
||||||
|
|
||||||
@@ -143,8 +160,7 @@ def build_extracted_edges(pages: list[Path]) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: dict) -> list[dict]:
|
def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: dict) -> list[dict]:
|
||||||
"""Pass 2: Claude-inferred semantic relationships."""
|
"""Pass 2: API-inferred semantic relationships."""
|
||||||
client = anthropic.Anthropic()
|
|
||||||
new_edges = []
|
new_edges = []
|
||||||
|
|
||||||
# Only process pages that changed since last run
|
# Only process pages that changed since last run
|
||||||
@@ -172,12 +188,7 @@ def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: d
|
|||||||
content = read_file(p)[:2000] # truncate for context efficiency
|
content = read_file(p)[:2000] # truncate for context efficiency
|
||||||
src = page_id(p)
|
src = page_id(p)
|
||||||
|
|
||||||
response = client.messages.create(
|
prompt = f"""Analyze this wiki page and identify implicit semantic relationships to other pages in the wiki.
|
||||||
model="claude-haiku-4-5-20251001",
|
|
||||||
max_tokens=1024,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": f"""Analyze this wiki page and identify implicit semantic relationships to other pages in the wiki.
|
|
||||||
|
|
||||||
Source page: {src}
|
Source page: {src}
|
||||||
Content:
|
Content:
|
||||||
@@ -200,10 +211,8 @@ Rules:
|
|||||||
- Do not repeat edges already in the extracted list
|
- Do not repeat edges already in the extracted list
|
||||||
- Return empty array [] if no new relationships found
|
- Return empty array [] if no new relationships found
|
||||||
"""
|
"""
|
||||||
}]
|
raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=1024)
|
||||||
)
|
raw = raw.strip()
|
||||||
|
|
||||||
raw = response.content[0].text.strip()
|
|
||||||
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
||||||
raw = re.sub(r"\s*```$", "", raw)
|
raw = re.sub(r"\s*```$", "", raw)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import anthropic
|
import os
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).parent.parent
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
WIKI_DIR = REPO_ROOT / "wiki"
|
WIKI_DIR = REPO_ROOT / "wiki"
|
||||||
@@ -41,6 +41,22 @@ def read_file(path: Path) -> str:
|
|||||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm(prompt: str, max_tokens: int = 8192) -> str:
|
||||||
|
try:
|
||||||
|
from litellm import completion
|
||||||
|
except ImportError:
|
||||||
|
print("Error: litellm not installed. Run: pip install litellm")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = os.getenv("LLM_MODEL", "claude-3-5-sonnet-latest")
|
||||||
|
response = completion(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=max_tokens
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
|
||||||
def write_file(path: Path, content: str):
|
def write_file(path: Path, content: str):
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(content, encoding="utf-8")
|
path.write_text(content, encoding="utf-8")
|
||||||
@@ -105,7 +121,7 @@ def ingest(source_path: str):
|
|||||||
wiki_context = build_wiki_context()
|
wiki_context = build_wiki_context()
|
||||||
schema = read_file(SCHEMA_FILE)
|
schema = read_file(SCHEMA_FILE)
|
||||||
|
|
||||||
client = anthropic.Anthropic()
|
schema = read_file(SCHEMA_FILE)
|
||||||
|
|
||||||
prompt = f"""You are maintaining an LLM Wiki. Process this source document and integrate its knowledge into the wiki.
|
prompt = f"""You are maintaining an LLM Wiki. Process this source document and integrate its knowledge into the wiki.
|
||||||
|
|
||||||
@@ -140,14 +156,8 @@ Return ONLY a valid JSON object with these fields (no markdown fences, no prose
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(" calling Claude API...")
|
print(f" calling API (model: ...)")
|
||||||
response = client.messages.create(
|
raw = call_llm(prompt, max_tokens=8192)
|
||||||
model="claude-sonnet-4-6",
|
|
||||||
max_tokens=8192,
|
|
||||||
messages=[{"role": "user", "content": prompt}],
|
|
||||||
)
|
|
||||||
|
|
||||||
raw = response.content[0].text
|
|
||||||
try:
|
try:
|
||||||
data = parse_json_from_response(raw)
|
data = parse_json_from_response(raw)
|
||||||
except (ValueError, json.JSONDecodeError) as e:
|
except (ValueError, json.JSONDecodeError) as e:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import anthropic
|
import os
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).parent.parent
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
WIKI_DIR = REPO_ROOT / "wiki"
|
WIKI_DIR = REPO_ROOT / "wiki"
|
||||||
@@ -33,6 +33,22 @@ def read_file(path: Path) -> str:
|
|||||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
|
||||||
|
try:
|
||||||
|
from litellm import completion
|
||||||
|
except ImportError:
|
||||||
|
print("Error: litellm not installed. Run: pip install litellm")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = os.getenv(model_env, default_model)
|
||||||
|
response = completion(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=max_tokens
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
|
||||||
def all_wiki_pages() -> list[Path]:
|
def all_wiki_pages() -> list[Path]:
|
||||||
return [p for p in WIKI_DIR.rglob("*.md")
|
return [p for p in WIKI_DIR.rglob("*.md")
|
||||||
if p.name not in ("index.md", "log.md", "lint-report.md")]
|
if p.name not in ("index.md", "log.md", "lint-report.md")]
|
||||||
@@ -112,14 +128,8 @@ def run_lint():
|
|||||||
rel = p.relative_to(REPO_ROOT)
|
rel = p.relative_to(REPO_ROOT)
|
||||||
pages_context += f"\n\n### {rel}\n{read_file(p)[:1500]}" # truncate long pages
|
pages_context += f"\n\n### {rel}\n{read_file(p)[:1500]}" # truncate long pages
|
||||||
|
|
||||||
client = anthropic.Anthropic()
|
print(" running semantic lint via API...")
|
||||||
print(" running semantic lint via Claude API...")
|
prompt = f"""You are linting an LLM Wiki. Review the pages below and identify:
|
||||||
response = client.messages.create(
|
|
||||||
model="claude-sonnet-4-6",
|
|
||||||
max_tokens=3000,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": f"""You are linting an LLM Wiki. Review the pages below and identify:
|
|
||||||
1. Contradictions between pages (claims that conflict)
|
1. Contradictions between pages (claims that conflict)
|
||||||
2. Stale content (summaries that newer sources have superseded)
|
2. Stale content (summaries that newer sources have superseded)
|
||||||
3. Data gaps (important questions the wiki can't answer — suggest specific sources to find)
|
3. Data gaps (important questions the wiki can't answer — suggest specific sources to find)
|
||||||
@@ -136,10 +146,7 @@ Return a markdown lint report with these sections:
|
|||||||
|
|
||||||
Be specific — name the exact pages and claims involved.
|
Be specific — name the exact pages and claims involved.
|
||||||
"""
|
"""
|
||||||
}]
|
semantic_report = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=3000)
|
||||||
)
|
|
||||||
|
|
||||||
semantic_report = response.content[0].text
|
|
||||||
|
|
||||||
# Compose full report
|
# Compose full report
|
||||||
report_lines = [
|
report_lines = [
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import argparse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import anthropic
|
import os
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).parent.parent
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
WIKI_DIR = REPO_ROOT / "wiki"
|
WIKI_DIR = REPO_ROOT / "wiki"
|
||||||
@@ -38,6 +38,22 @@ def write_file(path: Path, content: str):
|
|||||||
print(f" saved: {path.relative_to(REPO_ROOT)}")
|
print(f" saved: {path.relative_to(REPO_ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
|
||||||
|
try:
|
||||||
|
from litellm import completion
|
||||||
|
except ImportError:
|
||||||
|
print("Error: litellm not installed. Run: pip install litellm")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = os.getenv(model_env, default_model)
|
||||||
|
response = completion(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=max_tokens
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
|
||||||
def find_relevant_pages(question: str, index_content: str) -> list[Path]:
|
def find_relevant_pages(question: str, index_content: str) -> list[Path]:
|
||||||
"""Extract linked pages from index that seem relevant to the question."""
|
"""Extract linked pages from index that seem relevant to the question."""
|
||||||
# Pull all [[links]] and markdown links from index
|
# Pull all [[links]] and markdown links from index
|
||||||
@@ -64,7 +80,6 @@ def append_log(entry: str):
|
|||||||
|
|
||||||
def query(question: str, save_path: str | None = None):
|
def query(question: str, save_path: str | None = None):
|
||||||
today = date.today().isoformat()
|
today = date.today().isoformat()
|
||||||
client = anthropic.Anthropic()
|
|
||||||
|
|
||||||
# Step 1: Read index
|
# Step 1: Read index
|
||||||
index_content = read_file(INDEX_FILE)
|
index_content = read_file(INDEX_FILE)
|
||||||
@@ -77,16 +92,10 @@ def query(question: str, save_path: str | None = None):
|
|||||||
|
|
||||||
# If no keyword match, ask Claude to identify relevant pages from the index
|
# If no keyword match, ask Claude to identify relevant pages from the index
|
||||||
if not relevant_pages or len(relevant_pages) <= 1:
|
if not relevant_pages or len(relevant_pages) <= 1:
|
||||||
print(" selecting relevant pages via Claude...")
|
print(" selecting relevant pages via API...")
|
||||||
selection_response = client.messages.create(
|
prompt = f"Given this wiki index:\n\n{index_content}\n\nWhich pages are most relevant to answering: \"{question}\"\n\nReturn ONLY a JSON array of relative file paths (as listed in the index), e.g. [\"sources/foo.md\", \"concepts/Bar.md\"]. Maximum 10 pages."
|
||||||
model="claude-haiku-4-5-20251001",
|
raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=512)
|
||||||
max_tokens=512,
|
raw = raw.strip()
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": f"Given this wiki index:\n\n{index_content}\n\nWhich pages are most relevant to answering: \"{question}\"\n\nReturn ONLY a JSON array of relative file paths (as listed in the index), e.g. [\"sources/foo.md\", \"concepts/Bar.md\"]. Maximum 10 pages."
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
raw = selection_response.content[0].text.strip()
|
|
||||||
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
||||||
raw = re.sub(r"\s*```$", "", raw)
|
raw = re.sub(r"\s*```$", "", raw)
|
||||||
try:
|
try:
|
||||||
@@ -108,12 +117,7 @@ def query(question: str, save_path: str | None = None):
|
|||||||
|
|
||||||
# Step 4: Synthesize answer
|
# Step 4: Synthesize answer
|
||||||
print(f" synthesizing answer from {len(relevant_pages)} pages...")
|
print(f" synthesizing answer from {len(relevant_pages)} pages...")
|
||||||
response = client.messages.create(
|
prompt = f"""You are querying an LLM Wiki to answer a question. Use the wiki pages below to synthesize a thorough answer. Cite sources using [[PageName]] wikilink syntax.
|
||||||
model="claude-sonnet-4-6",
|
|
||||||
max_tokens=4096,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": f"""You are querying an LLM Wiki to answer a question. Use the wiki pages below to synthesize a thorough answer. Cite sources using [[PageName]] wikilink syntax.
|
|
||||||
|
|
||||||
Schema:
|
Schema:
|
||||||
{schema}
|
{schema}
|
||||||
@@ -125,10 +129,7 @@ Question: {question}
|
|||||||
|
|
||||||
Write a well-structured markdown answer with headers, bullets, and [[wikilink]] citations. At the end, add a ## Sources section listing the pages you drew from.
|
Write a well-structured markdown answer with headers, bullets, and [[wikilink]] citations. At the end, add a ## Sources section listing the pages you drew from.
|
||||||
"""
|
"""
|
||||||
}]
|
answer = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=4096)
|
||||||
)
|
|
||||||
|
|
||||||
answer = response.content[0].text
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print(answer)
|
print(answer)
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|||||||
Reference in New Issue
Block a user