Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Separate Knowledge system + Multi-Agent RAG Optimization #1620

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
63d4711
rag knowledge/multi agent
azep-ninja Dec 31, 2024
f334dd5
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Dec 31, 2024
304c1af
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 1, 2025
def7c6d
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 2, 2025
fc68576
Fixed character validation
azep-ninja Jan 2, 2025
e409b6d
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 2, 2025
3cc81e1
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 2, 2025
5dfb532
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 2, 2025
08f0bd5
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 3, 2025
2bfcfbf
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 3, 2025
480896d
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 5, 2025
2dab9e8
fxx dcker
azep-ninja Jan 5, 2025
458f4e7
Merge branch 'feat/seperate-knowledge-multi-agent-rag' of https://git…
azep-ninja Jan 5, 2025
5010a89
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 5, 2025
98e564b
sync
azep-ninja Jan 5, 2025
f8d9923
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 6, 2025
87b35e0
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 6, 2025
f91aa73
sync
azep-ninja Jan 6, 2025
cc2f759
Merge branch 'feat/seperate-knowledge-multi-agent-rag' of https://git…
azep-ninja Jan 6, 2025
66f6f6e
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 6, 2025
0391a7c
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 6, 2025
5b81200
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
shakkernerd Jan 6, 2025
3c41a8d
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
wtfsayo Jan 6, 2025
78296f6
Merge branch 'develop' into feat/seperate-knowledge-multi-agent-rag
azep-ninja Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,63 @@ CREATE TABLE IF NOT EXISTS "public"."rooms" (
"createdAt" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL
);

CREATE OR REPLACE FUNCTION "public"."search_knowledge"(
"query_embedding" "extensions"."vector",
"query_agent_id" "uuid",
"match_threshold" double precision,
"match_count" integer,
"search_text" text
) RETURNS TABLE (
"id" "uuid",
"agentId" "uuid",
"content" "jsonb",
"embedding" "extensions"."vector",
"createdAt" timestamp with time zone,
"similarity" double precision
) LANGUAGE "plpgsql" AS $$
BEGIN
RETURN QUERY
WITH vector_matches AS (
SELECT id,
1 - (embedding <=> query_embedding) as vector_score
FROM knowledge
WHERE (agentId IS NULL AND isShared = true) OR agentId = query_agent_id
AND embedding IS NOT NULL
),
keyword_matches AS (
SELECT id,
CASE
WHEN content->>'text' ILIKE '%' || search_text || '%' THEN 3.0
ELSE 1.0
END *
CASE
WHEN content->'metadata'->>'isChunk' = 'true' THEN 1.5
WHEN content->'metadata'->>'isMain' = 'true' THEN 1.2
ELSE 1.0
END as keyword_score
FROM knowledge
WHERE (agentId IS NULL AND isShared = true) OR agentId = query_agent_id
)
SELECT
k.id,
k."agentId",
k.content,
k.embedding,
k."createdAt",
(v.vector_score * kw.keyword_score) as similarity
FROM knowledge k
JOIN vector_matches v ON k.id = v.id
LEFT JOIN keyword_matches kw ON k.id = kw.id
WHERE (k.agentId IS NULL AND k.isShared = true) OR k.agentId = query_agent_id
AND (
v.vector_score >= match_threshold
OR (kw.keyword_score > 1.0 AND v.vector_score >= 0.3)
)
ORDER BY similarity DESC
LIMIT match_count;
END;
$$;

ALTER TABLE "public"."rooms" OWNER TO "postgres";

ALTER TABLE ONLY "public"."relationships"
Expand Down Expand Up @@ -564,6 +621,9 @@ ALTER TABLE ONLY "public"."relationships"
ALTER TABLE ONLY "public"."relationships"
ADD CONSTRAINT "relationships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."accounts"("id");

ALTER TABLE ONLY "public"."knowledge"
ADD CONSTRAINT "knowledge_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "public"."accounts"("id") ON DELETE CASCADE;

CREATE POLICY "Can select and update all data" ON "public"."accounts" USING (("auth"."uid"() = "id")) WITH CHECK (("auth"."uid"() = "id"));

CREATE POLICY "Enable delete for users based on userId" ON "public"."goals" FOR DELETE TO "authenticated" USING (("auth"."uid"() = "userId"));
Expand Down Expand Up @@ -600,6 +660,18 @@ CREATE POLICY "Enable update for users of own id" ON "public"."rooms" FOR UPDATE

CREATE POLICY "Enable users to delete their own relationships/friendships" ON "public"."relationships" FOR DELETE TO "authenticated" USING ((("auth"."uid"() = "userA") OR ("auth"."uid"() = "userB")));

CREATE POLICY "Enable read access for all users" ON "public"."knowledge"
FOR SELECT USING (true);

CREATE POLICY "Enable insert for authenticated users only" ON "public"."knowledge"
FOR INSERT TO "authenticated" WITH CHECK (true);

CREATE POLICY "Enable update for authenticated users" ON "public"."knowledge"
FOR UPDATE TO "authenticated" USING (true) WITH CHECK (true);

CREATE POLICY "Enable delete for users based on agentId" ON "public"."knowledge"
FOR DELETE TO "authenticated" USING (("auth"."uid"() = "agentId"));

ALTER TABLE "public"."accounts" ENABLE ROW LEVEL SECURITY;

ALTER TABLE "public"."goals" ENABLE ROW LEVEL SECURITY;
Expand All @@ -614,6 +686,8 @@ ALTER TABLE "public"."relationships" ENABLE ROW LEVEL SECURITY;

ALTER TABLE "public"."rooms" ENABLE ROW LEVEL SECURITY;

ALTER TABLE "public"."knowledge" ENABLE ROW LEVEL SECURITY;

CREATE POLICY "select_own_account" ON "public"."accounts" FOR SELECT USING (("auth"."uid"() = "id"));

GRANT USAGE ON SCHEMA "public" TO "postgres";
Expand Down Expand Up @@ -703,14 +777,18 @@ GRANT ALL ON TABLE "public"."secrets" TO "service_role";
GRANT ALL ON TABLE "public"."secrets" TO "supabase_admin";
GRANT ALL ON TABLE "public"."secrets" TO "supabase_auth_admin";

GRANT ALL ON TABLE "public"."knowledge" TO "authenticated";
GRANT ALL ON TABLE "public"."knowledge" TO "service_role";
GRANT ALL ON TABLE "public"."knowledge" TO "supabase_admin";
GRANT ALL ON TABLE "public"."knowledge" TO "supabase_auth_admin";

GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "service_role";
GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "supabase_admin";
GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "supabase_auth_admin";

GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "service_role";
GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "service_role";
GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "supabase_admin";
GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "supabase_auth_admin";

Expand All @@ -733,4 +811,9 @@ ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TAB
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "supabase_admin";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "supabase_auth_admin";

GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "authenticated";
GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "service_role";
GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "supabase_admin";
GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "supabase_auth_admin";

RESET ALL;
35 changes: 30 additions & 5 deletions packages/adapter-postgres/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
-- DROP TABLE IF EXISTS memories CASCADE;
-- DROP TABLE IF EXISTS rooms CASCADE;
-- DROP TABLE IF EXISTS accounts CASCADE;
-- DROP TABLE IF EXISTS knowledge CASCADE;


CREATE EXTENSION IF NOT EXISTS vector;
Expand All @@ -24,9 +25,6 @@ BEGIN
-- Then check for Ollama
ELSIF current_setting('app.use_ollama_embedding', TRUE) = 'true' THEN
RETURN 1024; -- Ollama mxbai-embed-large dimension
-- Then check for GAIANET
ELSIF current_setting('app.use_gaianet_embedding', TRUE) = 'true' THEN
RETURN 768; -- Gaianet nomic-embed dimension
Comment on lines -27 to -29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any specific reason why this is removed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved!

ELSE
RETURN 384; -- BGE/Other embedding dimension
END IF;
Expand Down Expand Up @@ -130,11 +128,38 @@ CREATE TABLE IF NOT EXISTS cache (
PRIMARY KEY ("key", "agentId")
);

DO $$
DECLARE
vector_dim INTEGER;
BEGIN
vector_dim := get_embedding_dimension();

EXECUTE format('
CREATE TABLE IF NOT EXISTS knowledge (
"id" UUID PRIMARY KEY,
"agentId" UUID REFERENCES accounts("id"),
"content" JSONB NOT NULL,
"embedding" vector(%s),
"createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"isMain" BOOLEAN DEFAULT FALSE,
"originalId" UUID REFERENCES knowledge("id"),
"chunkIndex" INTEGER,
"isShared" BOOLEAN DEFAULT FALSE,
CHECK((isShared = true AND "agentId" IS NULL) OR (isShared = false AND "agentId" IS NOT NULL))
)', vector_dim);
END $$;

-- Indexes
CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw ("embedding" vector_cosine_ops);
CREATE INDEX IF NOT EXISTS idx_memories_type_room ON memories("type", "roomId");
CREATE INDEX IF NOT EXISTS idx_participants_user ON participants("userId");
CREATE INDEX IF NOT EXISTS idx_participants_room ON participants("roomId");
CREATE INDEX IF NOT EXISTS idx_relationships_users ON relationships("userA", "userB");

COMMIT;
CREATE INDEX IF NOT EXISTS idx_knowledge_agent ON knowledge("agentId");
CREATE INDEX IF NOT EXISTS idx_knowledge_agent_main ON knowledge("agentId", "isMain");
CREATE INDEX IF NOT EXISTS idx_knowledge_original ON knowledge("originalId");
CREATE INDEX IF NOT EXISTS idx_knowledge_created ON knowledge("agentId", "createdAt");
CREATE INDEX IF NOT EXISTS idx_knowledge_shared ON knowledge("isShared");
CREATE INDEX IF NOT EXISTS idx_knowledge_embedding ON knowledge USING ivfflat (embedding vector_cosine_ops);

COMMIT;
177 changes: 177 additions & 0 deletions packages/adapter-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getEmbeddingConfig,
DatabaseAdapter,
EmbeddingProvider,
RAGKnowledgeItem
} from "@elizaos/core";
import fs from "fs";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -1457,6 +1458,182 @@ export class PostgresDatabaseAdapter
}
}, "deleteCache");
}

async getKnowledge(params: {
id?: UUID;
agentId: UUID;
limit?: number;
query?: string;
}): Promise<RAGKnowledgeItem[]> {
return this.withDatabase(async () => {
let sql = `SELECT * FROM knowledge WHERE ("agentId" = $1 OR "isShared" = true)`;
const queryParams: any[] = [params.agentId];
let paramCount = 1;

if (params.id) {
paramCount++;
sql += ` AND id = $${paramCount}`;
queryParams.push(params.id);
}

if (params.limit) {
paramCount++;
sql += ` LIMIT $${paramCount}`;
queryParams.push(params.limit);
}

const { rows } = await this.pool.query(sql, queryParams);

return rows.map(row => ({
id: row.id,
agentId: row.agentId,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
embedding: row.embedding ? new Float32Array(row.embedding) : undefined,
createdAt: row.createdAt.getTime()
}));
}, "getKnowledge");
}

async searchKnowledge(params: {
agentId: UUID;
embedding: Float32Array;
match_threshold: number;
match_count: number;
searchText?: string;
}): Promise<RAGKnowledgeItem[]> {
return this.withDatabase(async () => {
const cacheKey = `embedding_${params.agentId}_${params.searchText}`;
const cachedResult = await this.getCache({
key: cacheKey,
agentId: params.agentId
});

if (cachedResult) {
return JSON.parse(cachedResult);
}

const vectorStr = `[${Array.from(params.embedding).join(",")}]`;

const sql = `
WITH vector_scores AS (
SELECT id,
1 - (embedding <-> $1::vector) as vector_score
FROM knowledge
WHERE ("agentId" IS NULL AND "isShared" = true) OR "agentId" = $2
AND embedding IS NOT NULL
),
keyword_matches AS (
SELECT id,
CASE
WHEN content->>'text' ILIKE $3 THEN 3.0
ELSE 1.0
END *
CASE
WHEN (content->'metadata'->>'isChunk')::boolean = true THEN 1.5
WHEN (content->'metadata'->>'isMain')::boolean = true THEN 1.2
ELSE 1.0
END as keyword_score
FROM knowledge
WHERE ("agentId" IS NULL AND "isShared" = true) OR "agentId" = $2
)
SELECT k.*,
v.vector_score,
kw.keyword_score,
(v.vector_score * kw.keyword_score) as combined_score
FROM knowledge k
JOIN vector_scores v ON k.id = v.id
LEFT JOIN keyword_matches kw ON k.id = kw.id
WHERE ("agentId" IS NULL AND "isShared" = true) OR k."agentId" = $2
AND (
v.vector_score >= $4
OR (kw.keyword_score > 1.0 AND v.vector_score >= 0.3)
)
ORDER BY combined_score DESC
LIMIT $5
`;

const { rows } = await this.pool.query(sql, [
vectorStr,
params.agentId,
`%${params.searchText || ''}%`,
params.match_threshold,
params.match_count
]);

const results = rows.map(row => ({
id: row.id,
agentId: row.agentId,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
embedding: row.embedding ? new Float32Array(row.embedding) : undefined,
createdAt: row.createdAt.getTime(),
similarity: row.combined_score
}));

await this.setCache({
key: cacheKey,
agentId: params.agentId,
value: JSON.stringify(results)
});

return results;
}, "searchKnowledge");
}

async createKnowledge(knowledge: RAGKnowledgeItem): Promise<void> {
return this.withDatabase(async () => {
const client = await this.pool.connect();
try {
await client.query('BEGIN');

const sql = `
INSERT INTO knowledge (
id, "agentId", content, embedding, "createdAt",
"isMain", "originalId", "chunkIndex", "isShared"
) VALUES ($1, $2, $3, $4, to_timestamp($5/1000.0), $6, $7, $8, $9)
ON CONFLICT (id) DO NOTHING
`;

const metadata = knowledge.content.metadata || {};
const vectorStr = knowledge.embedding ?
`[${Array.from(knowledge.embedding).join(",")}]` : null;

await client.query(sql, [
knowledge.id,
metadata.isShared ? null : knowledge.agentId,
knowledge.content,
vectorStr,
knowledge.createdAt || Date.now(),
metadata.isMain || false,
metadata.originalId || null,
metadata.chunkIndex || null,
metadata.isShared || false
]);

await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}, "createKnowledge");
}

async removeKnowledge(id: UUID): Promise<void> {
return this.withDatabase(async () => {
await this.pool.query('DELETE FROM knowledge WHERE id = $1', [id]);
}, "removeKnowledge");
}

async clearKnowledge(agentId: UUID, shared?: boolean): Promise<void> {
return this.withDatabase(async () => {
const sql = shared ?
'DELETE FROM knowledge WHERE ("agentId" = $1 OR "isShared" = true)' :
'DELETE FROM knowledge WHERE "agentId" = $1';

await this.pool.query(sql, [agentId]);
}, "clearKnowledge");
}
}

export default PostgresDatabaseAdapter;
Loading
Loading