Skip to content

Commit

Permalink
feat: big refactor of obsidian pipe, embed videos, clarify UI, make i…
Browse files Browse the repository at this point in the history
…t possible ot use 2 different models for analysis and logs
  • Loading branch information
louis030195 committed Feb 18, 2025
1 parent b12c7ff commit a9cc7a6
Show file tree
Hide file tree
Showing 12 changed files with 1,160 additions and 589 deletions.
2 changes: 1 addition & 1 deletion pipes/obsidian/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian",
"version": "0.1.20",
"version": "0.1.21",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
Expand Down
2 changes: 1 addition & 1 deletion pipes/obsidian/src/app/api/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export async function GET(request: Request) {
}

const settings = await settingsManager.getAll();
const initialPath = settings.customSettings?.obsidian?.path;
const initialPath = settings.customSettings?.obsidian?.vaultPath;

if (!initialPath) {
return NextResponse.json({ files: [] });
Expand Down
308 changes: 130 additions & 178 deletions pipes/obsidian/src/app/api/intelligence/route.ts
Original file line number Diff line number Diff line change
@@ -1,172 +1,11 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { generateObject } from "ai";
import { generateObject, generateText, jsonSchema } from "ai";
import { ollama } from "ollama-ai-provider";
import { pipe } from "@screenpipe/js";
import * as fs from "fs/promises";
import * as path from "path";

// rich schema for relationship intelligence
const contactSchema = z.object({
name: z.string(),
company: z.string().optional(),
lastInteraction: z.string(),
sentiment: z.number(), // -1 to 1
topics: z.array(z.string()),
nextSteps: z.array(z.string()),
});

const relationshipIntelligence = z.object({
contacts: z.array(contactSchema),
insights: z.object({
followUps: z.array(z.string()),
opportunities: z.array(z.string()),
}),
});

async function analyzeRelationships(
recentLogs: string,
model: string
): Promise<z.infer<typeof relationshipIntelligence>> {
const prompt = `analyze these work logs and create a comprehensive relationship intelligence report.
focus on:
- identifying key people and their roles
- tracking interaction patterns and sentiment
- spotting business opportunities
- suggesting follow-ups and introductions
- finding patterns in topics discussed
recent logs: ${recentLogs}
todays date: ${new Date().toISOString().split("T")[0]}
local time: ${new Date().toLocaleTimeString()}
timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}
return a detailed json object following this structure for relationship intelligence.
example response from you:
{
"contacts": [
{
"name": "John Doe",
"company": "Acme Inc.",
"lastInteraction": "2024-01-01",
"sentiment": 0.8,
"topics": ["sales", "marketing"],
"nextSteps": ["schedule a call", "send a follow-up email"]
}
],
"insights": {
"followUps": ["schedule a call", "send a follow-up email"],
"opportunities": ["schedule a call", "send a follow-up email"]
}
}
of course adapt the example response to the actual data you have, do not use John Doe in your example response, use the names and companies of the people you see in the logs.
`;

const provider = ollama(model);
console.log("prompt", prompt);
const response = await generateObject({
model: provider,
messages: [{ role: "user", content: prompt }],
schema: relationshipIntelligence,
maxRetries: 5,
});

console.log(response.object);
return response.object;
}

async function saveToGraph(
intelligence: z.infer<typeof relationshipIntelligence>,
obsidianPath: string
): Promise<string> {
// normalize path for cross-platform compatibility
const normalizedPath = path.normalize(obsidianPath);
const graphPath = path.join(normalizedPath, "relationship-graph");
await fs.mkdir(graphPath, { recursive: true });

// create markdown file with mermaid graph
let mermaidGraph = "```mermaid\ngraph TD\n";

// add nodes for each contact
intelligence.contacts.forEach((contact) => {
mermaidGraph += ` ${contact.name.replace(/\s+/g, "_")}["${
contact.name
}\n${contact.company || ""}"]\n`;
});

// add basic relationships between contacts that share topics
const contactsByTopic = new Map<string, string[]>();
intelligence.contacts.forEach((contact) => {
contact.topics.forEach((topic) => {
if (!contactsByTopic.has(topic)) {
contactsByTopic.set(topic, []);
}
contactsByTopic.get(topic)?.push(contact.name);
});
});

// create edges for contacts sharing topics
contactsByTopic.forEach((contacts) => {
for (let i = 0; i < contacts.length; i++) {
for (let j = i + 1; j < contacts.length; j++) {
mermaidGraph += ` ${contacts[i].replace(/\s+/g, "_")} --- ${contacts[
j
].replace(/\s+/g, "_")}\n`;
}
}
});

mermaidGraph += "```\n";

// save as markdown with frontmatter
const content = `---
created: ${new Date().toISOString()}
tags: [relationship-intelligence, crm, network]
---
# relationship intelligence report
## network graph
${mermaidGraph}
## key contacts
${intelligence.contacts
.map(
(c) => `
### ${c.name}
- company: ${c.company || "n/a"}
- last interaction: ${c.lastInteraction}
- sentiment: ${c.sentiment}
- topics: ${c.topics.join(", ")}
- next steps: ${c.nextSteps.join(", ")}
`
)
.join("\n")}
## insights
### follow-ups needed
${intelligence.insights.followUps.map((f) => `- ${f}`).join("\n")}
### opportunities
${intelligence.insights.opportunities.map((o) => `- ${o}`).join("\n")}
`;

const filename = `${new Date().toISOString().split("T")[0]}-intelligence.md`;
await fs.writeFile(path.join(graphPath, filename), content, "utf8");

// get vault name safely for windows paths
const relativePath = obsidianPath
.replace(normalizedPath, "")
.replace(/^\//, "");
// Return the deep link
return `obsidian://search?vault=${encodeURIComponent(
relativePath
)}&query=relationship-intelligence`;
}
import { extractLinkedContent } from "@/lib/actions/obsidian";

async function readRecentLogs(
obsidianPath: string,
Expand All @@ -176,7 +15,6 @@ async function readRecentLogs(
const yesterday = since.toISOString().split("T")[0];

try {
// just read today and yesterday's logs as raw text
const todayContent = await fs
.readFile(path.join(obsidianPath, `${today}.md`), "utf8")
.catch(() => "");
Expand All @@ -191,11 +29,106 @@ async function readRecentLogs(
}
}

async function analyzeWithLLM(
content: string,
prompt: string,
model: string,
obsidianPath?: string
): Promise<any> {
const provider = ollama(model);

let enrichedPrompt = prompt;
if (obsidianPath) {
enrichedPrompt = await extractLinkedContent(prompt, obsidianPath);
}

const systemPrompt = `You are an intelligent analysis system that processes user activity logs and creates higher-level insights.
Context:
- You are analyzing logs that were collected every 5 minutes
- Your task is to create a higher-level synthesis (hourly/daily summaries)
- Previous logs contain important context for understanding patterns
- Look for recurring themes, projects, and behavioral patterns
- Pay special attention to @[[note references]] which indicate important user context
Here are some user instructions:
${enrichedPrompt}
Previous Logs:
${content}
Instructions for Media and Formatting:
- When referencing videos, use: <video src="file:///PATH_TO_VIDEO.mp4" controls/>
- For links to other notes, use: [[note-name]]
- For timestamps, use: \`HH:MM\` format
- Create sections with level-3 headers: ### Section Name
- Use bullet points for lists and patterns
- Do not wrap your response in \`\`\`markdown\`\`\` tags
- Add relevant #tags for categorization
- Escape any pipe characters (|) in tables with \\|
- All your outputs will be written directly in Obsidian note taking app so use the formatting of the app in your response to maximize readability,
embeding links, videos, etc. usually mp4 needs video html component and not the link format
Analysis Structure:
### Summary
- High-level overview of the time period
- Key accomplishments and patterns
### Activity Timeline
- Chronological breakdown of significant events
- Include relevant media embeddings
### Patterns & Insights
- Recurring themes or behaviors
- Project progress
- Context switches
- Time allocation
### Related Notes
- Link to relevant vault notes
- Context connections
Generate a structured analysis following the above format.`;

console.log("systemPrompt", systemPrompt);

const response = await generateText({
model: provider,
messages: [{ role: "user", content: systemPrompt }],
maxRetries: 5,
});

return response.text;
}

async function saveMarkdown(
content: string,
obsidianPath: string,
filename: string
): Promise<string> {
const normalizedPath = path.normalize(obsidianPath);
await fs.mkdir(normalizedPath, { recursive: true });

const filePath = path.join(normalizedPath, filename);
await fs.writeFile(filePath, content, "utf8");

const relativePath = obsidianPath
.replace(normalizedPath, "")
.replace(/^\//, "");
return `obsidian://open?vault=${encodeURIComponent(
relativePath
)}&file=${encodeURIComponent(filename)}`;
}

export async function GET() {
try {
const settings = await pipe.settings.getAll();
const obsidianPath = settings.customSettings?.obsidian?.vaultPath;
const model = settings.customSettings?.obsidian?.aiModel;
const model = settings.customSettings?.obsidian?.analysisModel;
const customPrompt = settings.customSettings?.obsidian?.prompt;
const timeWindow =
settings.customSettings?.obsidian?.analysisTimeWindow ||
1 * 60 * 60 * 1000;

if (!obsidianPath) {
return NextResponse.json(
Expand All @@ -204,28 +137,47 @@ export async function GET() {
);
}

// get last 24 hours of logs
const today = new Date();
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
// Read logs within time window
const now = new Date();
const since = new Date(now.getTime() - timeWindow);
const recentLogs = await readRecentLogs(obsidianPath, since);

const recentLogs = await readRecentLogs(obsidianPath, yesterday);

if (recentLogs.length === 0) {
if (!recentLogs) {
return NextResponse.json({ message: "no logs found for analysis" });
}

const intelligence = await analyzeRelationships(recentLogs, model);
const deepLink = await saveToGraph(intelligence, obsidianPath);
// Analyze with LLM
const analysis = await analyzeWithLLM(
recentLogs,
customPrompt,
model,
obsidianPath
);

// Save results based on output format
const filename = `${now
.toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.split("/")
.reverse()
.join("-")}-${now.getHours().toString().padStart(2, "0")}-${now
.getMinutes()
.toString()
.padStart(2, "0")}-analysis.md`;

const deepLink = await saveMarkdown(analysis, obsidianPath, filename);

return NextResponse.json({
message: "relationship intelligence updated",
intelligence,
message: "analysis completed",
intelligence: analysis,
deepLink,
summary: {
contacts: intelligence.contacts.length,
opportunities: intelligence.insights.opportunities.length,
needsFollowUp: intelligence.insights.followUps.length,
timeWindow,
logsAnalyzed: recentLogs.length,
timestamp: now.toISOString(),
},
});
} catch (error) {
Expand Down
Loading

0 comments on commit a9cc7a6

Please sign in to comment.