mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 23:48:26 +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
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
docker-compose.dev.yaml
|
||||
@ -53,6 +53,7 @@ export const Ingest = () => {
|
||||
placeholder="Tell what you want to add"
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="max-h-[400px]"
|
||||
/>
|
||||
|
||||
<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
|
||||
.forceSimulation(nodes as d3.SimulationNodeDatum[])
|
||||
.force(
|
||||
@ -374,41 +392,46 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
|
||||
d3
|
||||
.forceLink(links)
|
||||
.id((d: any) => d.id)
|
||||
.distance(200)
|
||||
.strength(0.2),
|
||||
.distance(LINK_DISTANCE)
|
||||
.strength(LINK_STRENGTH),
|
||||
)
|
||||
.force(
|
||||
"charge",
|
||||
d3
|
||||
.forceManyBody()
|
||||
.strength((d: any) => {
|
||||
// Use a less negative strength for isolated nodes
|
||||
// to pull them closer to the center
|
||||
return isolatedNodeIds.has(d.id) ? -500 : -3000;
|
||||
})
|
||||
.strength((d: any) =>
|
||||
isolatedNodeIds.has(d.id) ? CHARGE_ISOLATED : CHARGE_CONNECTED,
|
||||
)
|
||||
.distanceMin(20)
|
||||
.distanceMax(500)
|
||||
.theta(0.8),
|
||||
.distanceMax(600)
|
||||
.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(
|
||||
"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(
|
||||
"isolatedGravity",
|
||||
d3
|
||||
.forceRadial(
|
||||
100, // distance from center
|
||||
width / 2, // center x
|
||||
height / 2, // center y
|
||||
)
|
||||
.strength((d: any) => (isolatedNodeIds.has(d.id) ? 0.15 : 0.01)),
|
||||
.forceRadial(ISOLATED_RADIAL_DIST, width / 2, height / 2)
|
||||
.strength((d: any) =>
|
||||
isolatedNodeIds.has(d.id)
|
||||
? ISOLATED_RADIAL_STRENGTH
|
||||
: NONISOLATED_RADIAL_STRENGTH,
|
||||
),
|
||||
)
|
||||
.velocityDecay(0.4)
|
||||
.alphaDecay(0.05)
|
||||
.alphaMin(0.001);
|
||||
.velocityDecay(VELOCITY_DECAY)
|
||||
.alphaDecay(ALPHA_DECAY)
|
||||
.alphaMin(ALPHA_MIN);
|
||||
|
||||
simulationRef.current = simulation;
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ export function getUserQueue(userId: string) {
|
||||
export const IngestBodyRequest = z.object({
|
||||
episodeBody: 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(),
|
||||
spaceId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
import { parse } from "@conform-to/zod";
|
||||
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 { Ingest } from "~/components/dashboard/ingest";
|
||||
import {
|
||||
@ -23,6 +23,7 @@ import { Search } from "~/components/dashboard";
|
||||
import { SearchBodyRequest } from "./search";
|
||||
import { SearchService } from "~/services/search.server";
|
||||
|
||||
// --- Only return userId in loader, fetch nodeLinks on client ---
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const formData = await request.formData();
|
||||
@ -52,17 +53,46 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// Only return userId, not the heavy nodeLinks
|
||||
const userId = await requireUserId(request);
|
||||
const nodeLinks = await getNodeLinks(userId);
|
||||
|
||||
return nodeLinks;
|
||||
return { userId };
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const nodeLinks = useTypedLoaderData<typeof loader>();
|
||||
|
||||
const { userId } = useTypedLoaderData<typeof loader>();
|
||||
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 (
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel
|
||||
@ -75,9 +105,15 @@ export default function Dashboard() {
|
||||
<h3 className="text-lg font-medium">Graph</h3>
|
||||
<p className="text-muted-foreground"> Your memory graph </p>
|
||||
|
||||
<div className="bg-background-3 mt-2 grow rounded">
|
||||
{typeof window !== "undefined" && (
|
||||
<GraphVisualization triplets={nodeLinks} />
|
||||
<div className="bg-background-3 mt-2 flex grow items-center justify-center rounded">
|
||||
{loading ? (
|
||||
<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>
|
||||
@ -85,7 +121,7 @@ export default function Dashboard() {
|
||||
<ResizableHandle className="bg-border w-[0.5px]" />
|
||||
|
||||
<ResizablePanel
|
||||
className="rounded-md"
|
||||
className="overflow-auto"
|
||||
collapsible={false}
|
||||
maxSize={50}
|
||||
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 },
|
||||
provenance,
|
||||
};
|
||||
console.log("Triple (no embedding):", JSON.stringify(safeTriple));
|
||||
}
|
||||
// console.log("Invalidated statements", invalidatedStatements);
|
||||
|
||||
// Save triples sequentially to avoid parallel processing issues
|
||||
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
|
||||
const messages = extractAttributes(context);
|
||||
|
||||
|
||||
@ -623,9 +623,12 @@ async function wrapResponse(
|
||||
response: Response,
|
||||
useCors: boolean,
|
||||
): Promise<Response> {
|
||||
return useCors
|
||||
? await apiCors(request, response, {
|
||||
exposedHeaders: ["x-sol-jwt", "x-sol-jwt-claims"],
|
||||
})
|
||||
: response;
|
||||
// Prevent double CORS headers by checking if already present
|
||||
if (useCors && !response.headers.has("access-control-allow-origin")) {
|
||||
return await apiCors(request, response, {
|
||||
exposedHeaders: ["x-sol-jwt", "x-sol-jwt-claims"],
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -36,10 +36,6 @@ export function useMatchesData(
|
||||
): UIMatch | undefined {
|
||||
const matchingRoutes = useMatches();
|
||||
|
||||
if (debug) {
|
||||
console.log("matchingRoutes", matchingRoutes);
|
||||
}
|
||||
|
||||
const paths = Array.isArray(id) ? id : [id];
|
||||
|
||||
// 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