mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 18:38:27 +00:00
Merge pull request #1 from RedPlanetHQ/harshith/graph
enh: memory graph visualisation
This commit is contained in:
commit
cae5470c70
2
.gitignore
vendored
2
.gitignore
vendored
@ -36,3 +36,5 @@ yarn-error.log*
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
docker-compose.dev.yaml
|
||||||
@ -53,6 +53,7 @@ export const Ingest = () => {
|
|||||||
placeholder="Tell what you want to add"
|
placeholder="Tell what you want to add"
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
className="max-h-[400px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
|
|||||||
@ -366,7 +366,25 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create simulation with custom forces
|
// Enhanced simulation for improved aesthetics and readability
|
||||||
|
|
||||||
|
// Parameters tuned for clear separation of clusters and minimal overlap,
|
||||||
|
// as seen in the provided image (distinct, well-separated groups).
|
||||||
|
const LINK_DISTANCE = 120; // Slightly shorter for tighter clusters
|
||||||
|
const LINK_STRENGTH = 0.6; // Stronger to keep clusters compact
|
||||||
|
const CHARGE_ISOLATED = -200; // Less repulsion for isolated nodes (keeps them closer)
|
||||||
|
const CHARGE_CONNECTED = -1000; // Strong repulsion for connected nodes (prevents crowding)
|
||||||
|
const COLLIDE_RADIUS = 32; // Smaller collision radius for less overlap
|
||||||
|
const COLLIDE_STRENGTH = 0.9; // Stronger collision to avoid overlap
|
||||||
|
const COLLIDE_ITER = 10; // More iterations for better separation
|
||||||
|
const CENTER_STRENGTH = 0.18; // Pull clusters more to center
|
||||||
|
const ISOLATED_RADIAL_DIST = 260; // Place isolated nodes further from center
|
||||||
|
const ISOLATED_RADIAL_STRENGTH = 0.28; // Stronger pull for isolated nodes
|
||||||
|
const NONISOLATED_RADIAL_STRENGTH = 0.06; // Slight pull for non-isolated
|
||||||
|
const VELOCITY_DECAY = 0.28; // Smoother, more stable layout
|
||||||
|
const ALPHA_DECAY = 0.035;
|
||||||
|
const ALPHA_MIN = 0.001;
|
||||||
|
|
||||||
const simulation = d3
|
const simulation = d3
|
||||||
.forceSimulation(nodes as d3.SimulationNodeDatum[])
|
.forceSimulation(nodes as d3.SimulationNodeDatum[])
|
||||||
.force(
|
.force(
|
||||||
@ -374,41 +392,46 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
|
|||||||
d3
|
d3
|
||||||
.forceLink(links)
|
.forceLink(links)
|
||||||
.id((d: any) => d.id)
|
.id((d: any) => d.id)
|
||||||
.distance(200)
|
.distance(LINK_DISTANCE)
|
||||||
.strength(0.2),
|
.strength(LINK_STRENGTH),
|
||||||
)
|
)
|
||||||
.force(
|
.force(
|
||||||
"charge",
|
"charge",
|
||||||
d3
|
d3
|
||||||
.forceManyBody()
|
.forceManyBody()
|
||||||
.strength((d: any) => {
|
.strength((d: any) =>
|
||||||
// Use a less negative strength for isolated nodes
|
isolatedNodeIds.has(d.id) ? CHARGE_ISOLATED : CHARGE_CONNECTED,
|
||||||
// to pull them closer to the center
|
)
|
||||||
return isolatedNodeIds.has(d.id) ? -500 : -3000;
|
|
||||||
})
|
|
||||||
.distanceMin(20)
|
.distanceMin(20)
|
||||||
.distanceMax(500)
|
.distanceMax(600)
|
||||||
.theta(0.8),
|
.theta(0.9),
|
||||||
|
)
|
||||||
|
.force(
|
||||||
|
"center",
|
||||||
|
d3.forceCenter(width / 2, height / 2).strength(CENTER_STRENGTH),
|
||||||
)
|
)
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.05))
|
|
||||||
.force(
|
.force(
|
||||||
"collide",
|
"collide",
|
||||||
d3.forceCollide().radius(50).strength(0.3).iterations(5),
|
d3
|
||||||
|
.forceCollide()
|
||||||
|
.radius(COLLIDE_RADIUS)
|
||||||
|
.strength(COLLIDE_STRENGTH)
|
||||||
|
.iterations(COLLIDE_ITER),
|
||||||
)
|
)
|
||||||
// Add a special gravity force for isolated nodes to pull them toward the center
|
// Special gravity for isolated nodes to keep them separated and visible
|
||||||
.force(
|
.force(
|
||||||
"isolatedGravity",
|
"isolatedGravity",
|
||||||
d3
|
d3
|
||||||
.forceRadial(
|
.forceRadial(ISOLATED_RADIAL_DIST, width / 2, height / 2)
|
||||||
100, // distance from center
|
.strength((d: any) =>
|
||||||
width / 2, // center x
|
isolatedNodeIds.has(d.id)
|
||||||
height / 2, // center y
|
? ISOLATED_RADIAL_STRENGTH
|
||||||
)
|
: NONISOLATED_RADIAL_STRENGTH,
|
||||||
.strength((d: any) => (isolatedNodeIds.has(d.id) ? 0.15 : 0.01)),
|
),
|
||||||
)
|
)
|
||||||
.velocityDecay(0.4)
|
.velocityDecay(VELOCITY_DECAY)
|
||||||
.alphaDecay(0.05)
|
.alphaDecay(ALPHA_DECAY)
|
||||||
.alphaMin(0.001);
|
.alphaMin(ALPHA_MIN);
|
||||||
|
|
||||||
simulationRef.current = simulation;
|
simulationRef.current = simulation;
|
||||||
|
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export function getUserQueue(userId: string) {
|
|||||||
export const IngestBodyRequest = z.object({
|
export const IngestBodyRequest = z.object({
|
||||||
episodeBody: z.string(),
|
episodeBody: z.string(),
|
||||||
referenceTime: z.string(),
|
referenceTime: z.string(),
|
||||||
metadata: z.record(z.union([z.string(), z.number()])),
|
metadata: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
spaceId: z.string().optional(),
|
spaceId: z.string().optional(),
|
||||||
sessionId: z.string().optional(),
|
sessionId: z.string().optional(),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
import { parse } from "@conform-to/zod";
|
import { parse } from "@conform-to/zod";
|
||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import { Ingest } from "~/components/dashboard/ingest";
|
import { Ingest } from "~/components/dashboard/ingest";
|
||||||
import {
|
import {
|
||||||
@ -23,6 +23,7 @@ import { Search } from "~/components/dashboard";
|
|||||||
import { SearchBodyRequest } from "./search";
|
import { SearchBodyRequest } from "./search";
|
||||||
import { SearchService } from "~/services/search.server";
|
import { SearchService } from "~/services/search.server";
|
||||||
|
|
||||||
|
// --- Only return userId in loader, fetch nodeLinks on client ---
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
@ -52,17 +53,46 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
// Only return userId, not the heavy nodeLinks
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
const nodeLinks = await getNodeLinks(userId);
|
return { userId };
|
||||||
|
|
||||||
return nodeLinks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const nodeLinks = useTypedLoaderData<typeof loader>();
|
const { userId } = useTypedLoaderData<typeof loader>();
|
||||||
|
|
||||||
const [size, setSize] = useState(15);
|
const [size, setSize] = useState(15);
|
||||||
|
|
||||||
|
// State for nodeLinks and loading
|
||||||
|
const [nodeLinks, setNodeLinks] = useState<any[] | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
async function fetchNodeLinks() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
"/node-links?userId=" + encodeURIComponent(userId),
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch node links");
|
||||||
|
const data = await res.json();
|
||||||
|
if (!cancelled) {
|
||||||
|
setNodeLinks(data);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setNodeLinks([]);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchNodeLinks();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
@ -75,9 +105,15 @@ export default function Dashboard() {
|
|||||||
<h3 className="text-lg font-medium">Graph</h3>
|
<h3 className="text-lg font-medium">Graph</h3>
|
||||||
<p className="text-muted-foreground"> Your memory graph </p>
|
<p className="text-muted-foreground"> Your memory graph </p>
|
||||||
|
|
||||||
<div className="bg-background-3 mt-2 grow rounded">
|
<div className="bg-background-3 mt-2 flex grow items-center justify-center rounded">
|
||||||
{typeof window !== "undefined" && (
|
{loading ? (
|
||||||
<GraphVisualization triplets={nodeLinks} />
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400" />
|
||||||
|
<span className="text-muted-foreground">Loading graph...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
nodeLinks && <GraphVisualization triplets={nodeLinks} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +121,7 @@ export default function Dashboard() {
|
|||||||
<ResizableHandle className="bg-border w-[0.5px]" />
|
<ResizableHandle className="bg-border w-[0.5px]" />
|
||||||
|
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
className="rounded-md"
|
className="overflow-auto"
|
||||||
collapsible={false}
|
collapsible={false}
|
||||||
maxSize={50}
|
maxSize={50}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
|
|||||||
10
apps/webapp/app/routes/node-links.tsx
Normal file
10
apps/webapp/app/routes/node-links.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||||
|
import { getNodeLinks } from "~/lib/neo4j.server";
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const userId = url.searchParams.get("userId");
|
||||||
|
if (!userId) return json([], { status: 400 });
|
||||||
|
const nodeLinks = await getNodeLinks(userId);
|
||||||
|
return json(nodeLinks);
|
||||||
|
}
|
||||||
@ -126,9 +126,7 @@ export class KnowledgeGraphService {
|
|||||||
statement: { ...statement, factEmbedding: undefined },
|
statement: { ...statement, factEmbedding: undefined },
|
||||||
provenance,
|
provenance,
|
||||||
};
|
};
|
||||||
console.log("Triple (no embedding):", JSON.stringify(safeTriple));
|
|
||||||
}
|
}
|
||||||
// console.log("Invalidated statements", invalidatedStatements);
|
|
||||||
|
|
||||||
// Save triples sequentially to avoid parallel processing issues
|
// Save triples sequentially to avoid parallel processing issues
|
||||||
for (const triple of updatedTriples) {
|
for (const triple of updatedTriples) {
|
||||||
@ -739,9 +737,6 @@ export class KnowledgeGraphService {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("entityTypes", JSON.stringify(entityTypes));
|
|
||||||
console.log("entities", JSON.stringify(context.entities));
|
|
||||||
|
|
||||||
// Create a prompt for the LLM to extract attributes
|
// Create a prompt for the LLM to extract attributes
|
||||||
const messages = extractAttributes(context);
|
const messages = extractAttributes(context);
|
||||||
|
|
||||||
|
|||||||
@ -623,9 +623,12 @@ async function wrapResponse(
|
|||||||
response: Response,
|
response: Response,
|
||||||
useCors: boolean,
|
useCors: boolean,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
return useCors
|
// Prevent double CORS headers by checking if already present
|
||||||
? await apiCors(request, response, {
|
if (useCors && !response.headers.has("access-control-allow-origin")) {
|
||||||
exposedHeaders: ["x-sol-jwt", "x-sol-jwt-claims"],
|
return await apiCors(request, response, {
|
||||||
})
|
exposedHeaders: ["x-sol-jwt", "x-sol-jwt-claims"],
|
||||||
: response;
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,10 +36,6 @@ export function useMatchesData(
|
|||||||
): UIMatch | undefined {
|
): UIMatch | undefined {
|
||||||
const matchingRoutes = useMatches();
|
const matchingRoutes = useMatches();
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log("matchingRoutes", matchingRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const paths = Array.isArray(id) ? id : [id];
|
const paths = Array.isArray(id) ? id : [id];
|
||||||
|
|
||||||
// Get the first matching route
|
// Get the first matching route
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
core:
|
|
||||||
container_name: core-app
|
|
||||||
image: redplanethq/core:${VERSION}
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=${NODE_ENV}
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- DIRECT_URL=${DIRECT_URL}
|
|
||||||
- SESSION_SECRET=${SESSION_SECRET}
|
|
||||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
|
||||||
- MAGIC_LINK_SECRET=${MAGIC_LINK_SECRET}
|
|
||||||
- LOGIN_ORIGIN=${LOGIN_ORIGIN}
|
|
||||||
- APP_ORIGIN=${APP_ORIGIN}
|
|
||||||
- REDIS_HOST=${REDIS_HOST}
|
|
||||||
- REDIS_PORT=${REDIS_PORT}
|
|
||||||
- REDIS_TLS_DISABLED=${REDIS_TLS_DISABLED}
|
|
||||||
- NEO4J_URI=${NEO4J_URI}
|
|
||||||
- NEO4J_USERNAME=${NEO4J_USERNAME}
|
|
||||||
- NEO4J_PASSWORD=${NEO4J_PASSWORD}
|
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
||||||
- AUTH_GOOGLE_CLIENT_ID=${AUTH_GOOGLE_CLIENT_ID}
|
|
||||||
- AUTH_GOOGLE_CLIENT_SECRET=${AUTH_GOOGLE_CLIENT_SECRET}
|
|
||||||
- ENABLE_EMAIL_LOGIN=${ENABLE_EMAIL_LOGIN}
|
|
||||||
ports:
|
|
||||||
- "3033:3000"
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- neo4j
|
|
||||||
networks:
|
|
||||||
- core
|
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: core-redis
|
|
||||||
image: redis:7
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
networks:
|
|
||||||
- core
|
|
||||||
|
|
||||||
neo4j:
|
|
||||||
container_name: core-neo4j
|
|
||||||
image: neo4j:5
|
|
||||||
environment:
|
|
||||||
- NEO4J_AUTH=${NEO4J_AUTH}
|
|
||||||
ports:
|
|
||||||
- "7474:7474"
|
|
||||||
- "7687:7687"
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /efs/neo4j
|
|
||||||
target: /data
|
|
||||||
networks:
|
|
||||||
- core
|
|
||||||
|
|
||||||
networks:
|
|
||||||
core:
|
|
||||||
driver: bridge
|
|
||||||
Loading…
x
Reference in New Issue
Block a user