@@ -32,13 +32,104 @@ export type SimilarityResult = {
3232export async function getOpenAiEmbedding ( text : string ) : Promise < Array < number > > {
3333 const openai = new OpenAI ( { apiKey : process . env . OPENAI_API_KEY } ) ;
3434 const response = await openai . embeddings . create ( {
35- model : "text-embedding-ada-002 " ,
35+ model : "text-embedding-3-small " ,
3636 input : text ,
3737 encoding_format : "float" ,
3838 } ) ;
3939 return response . data [ 0 ] . embedding ;
4040}
4141
42+ export async function textToCypher ( options :GraphModelOptions , text : string , ctoModel ) : Promise < string | null > {
43+ const openai = new OpenAI ( { apiKey : process . env . OPENAI_API_KEY } ) ;
44+
45+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46+ const messages :any = [ {
47+ role : 'system' ,
48+ content : `Convert the natural language query delimited by triple quotes to a Neo4J Cypher query.
49+ Just return the Cypher query, without an explanation for how it works. Do not enclose the result in a markdown code block.
50+ The nodes and edges in Neo4j are described via the following Accord Project Concerto model:
51+
52+ Concerto mode:
53+ \`\`\`
54+ ${ ctoModel }
55+ \`\`\`
56+
57+ Concerto properties with the @vector_index decorator have a Neo4J vector index. The name
58+ of the vector index is the lowercase name of the declaration with '_' and the lowercase
59+ name of the property appended.
60+
61+ Concerto declarations with any properties with the @fulltext_index decorator have a
62+ full text index. The name of the full text index is the lowercase name of the declaration
63+ with '_fulltext' appended.
64+
65+ Here is an example NeoJ4 query that matches 3 movies by conceptual similarity
66+ (using vector cosine similarity):
67+ MATCH (l:Movie)
68+ CALL db.index.vector.queryNodes('movie_summary', 3, [-0.042983294,-0.00888215, ...] )
69+ YIELD node AS similar, score
70+ MATCH (similar)
71+ RETURN similar.identifier as identifier, similar.summary as content, score limit 3
72+
73+ Natural language query: """${ text }
74+ """
75+ ` } ] ;
76+
77+ const EMBEDDINGS_MAGIC = '<EMBEDDINGS>' ;
78+
79+ const params : OpenAI . Chat . ChatCompletionCreateParams = {
80+ temperature : 0.1 ,
81+ tools : [
82+ {
83+ "type" : "function" ,
84+ "function" : {
85+ "name" : "get_embeddings" ,
86+ "description" : "Get vector embeddings for a query string" ,
87+ "parameters" : {
88+ "type" : "object" ,
89+ "properties" : {
90+ "query" : {
91+ "type" : "string" ,
92+ }
93+ } ,
94+ "required" : [ "query" ]
95+ }
96+ }
97+ }
98+ ] ,
99+ tool_choice : "auto" ,
100+ messages,
101+ model : 'gpt-4o' ,
102+ } ;
103+ let chatCompletion : OpenAI . Chat . ChatCompletion = await openai . chat . completions . create ( params ) ;
104+ if ( chatCompletion . choices [ 0 ] . message . tool_calls && options . embeddingFunction ) {
105+ if ( chatCompletion . choices [ 0 ] . message . tool_calls . length > 0 ) {
106+ const tool = chatCompletion . choices [ 0 ] . message . tool_calls [ 0 ] ;
107+ options . logger ?. log ( `Calling tool: ${ tool . function . name } ` ) ;
108+ if ( tool . function . name === 'get_embeddings' ) {
109+ messages . push ( chatCompletion . choices [ 0 ] . message ) ;
110+ messages . push ( {
111+ "role" :"tool" ,
112+ "tool_call_id" : tool . id ,
113+ "name" : tool . function . name ,
114+ "content" : EMBEDDINGS_MAGIC
115+ } )
116+ chatCompletion = await openai . chat . completions . create ( params ) ;
117+ const args = JSON . parse ( tool . function . arguments ) ;
118+ const embeddings = await options . embeddingFunction ( args . query ) ;
119+ if ( chatCompletion . choices [ 0 ] . message . content ) {
120+ options . logger ?. log ( `Tool replacing embeddings: ${ chatCompletion . choices [ 0 ] . message . content } ` ) ;
121+ chatCompletion . choices [ 0 ] . message . content = chatCompletion . choices [ 0 ] . message . content . replaceAll ( EMBEDDINGS_MAGIC , JSON . stringify ( embeddings ) ) ;
122+ }
123+ }
124+ else {
125+ throw new Error ( `Unrecognized tool: ${ tool . function } ` ) ;
126+ }
127+ }
128+ }
129+ return chatCompletion . choices [ 0 ] . message . content ;
130+ }
131+
132+
42133export function getObjectChecksum ( obj : PropertyBag ) {
43134 const deterministicReplacer = ( _ , v ) =>
44135 typeof v !== 'object' || v === null || Array . isArray ( v ) ? v :
@@ -431,34 +522,63 @@ export class GraphModel {
431522 }
432523 }
433524
434- async fullTextQuery ( typeName : string , searchText : string , count : number ) {
435- try {
436- const graphNode = this . getGraphNodeDeclaration ( typeName ) ;
437- const fullTextIndex = this . getFullTextIndex ( graphNode ) ;
438- if ( ! fullTextIndex ) {
439- throw new Error ( `No full text index for properties of ${ typeName } ` ) ;
525+ async textToCypher ( text : string ) : Promise < string | null > {
526+ const ctoModels = this . modelManager . getModels ( ) . reduce ( ( prev , cur ) => prev += cur . content , '' ) ;
527+ return textToCypher ( this . options , text , ctoModels ) ;
528+ }
529+
530+ async chatWithData ( text : string ) {
531+ const cypher = await this . textToCypher ( text ) ;
532+ if ( cypher ) {
533+ const context = await this . openSession ( ) ;
534+ const transaction = await context . session . beginTransaction ( ) ;
535+ try {
536+ const queryResult = await this . query ( cypher ) ;
537+ return queryResult ? queryResult . records . map ( v => {
538+ const result = { } ;
539+ v . keys . forEach ( k => {
540+ result [ k ] = v . get ( k ) ;
541+ } )
542+ return result ;
543+ } ) : [ ] ;
544+ }
545+ catch ( err ) {
546+ this . options . logger ?. log ( ( err as object ) . toString ( ) ) ;
547+ transaction ?. rollback ( ) ;
548+ throw err ;
440549 }
441- const indexName = this . getFullTextIndexName ( graphNode ) ;
442- const props = fullTextIndex . properties . map ( p => `node.${ p } ` ) ;
443- props . push ( 'node.identifier' ) ;
444- const q = `CALL db.index.fulltext.queryNodes("${ indexName } ", "${ searchText } ") YIELD node, score RETURN ${ props . join ( ',' ) } , score limit ${ count } ` ;
445- const queryResult = await this . query ( q ) ;
446- return queryResult ? queryResult . records . map ( v => {
447- const result = { } ;
448- fullTextIndex . properties . forEach ( p => {
449- result [ p ] = v . get ( `node.${ p } ` )
450- } ) ;
451- result [ 'score' ] = v . get ( 'score' ) ;
452- result [ 'identifier' ] = v . get ( 'node.identifier' ) ;
453- return result ;
454- } ) : [ ] ;
455- }
456- catch ( err ) {
457- this . options . logger ?. log ( ( err as object ) . toString ( ) ) ;
458- throw err ;
459550 }
551+ throw new Error ( `Failed to convert to Cypher query ${ text } ` ) ;
460552 }
461553
554+ async fullTextQuery ( typeName : string , searchText : string , count : number ) {
555+ try {
556+ const graphNode = this . getGraphNodeDeclaration ( typeName ) ;
557+ const fullTextIndex = this . getFullTextIndex ( graphNode ) ;
558+ if ( ! fullTextIndex ) {
559+ throw new Error ( `No full text index for properties of ${ typeName } ` ) ;
560+ }
561+ const indexName = this . getFullTextIndexName ( graphNode ) ;
562+ const props = fullTextIndex . properties . map ( p => `node.${ p } ` ) ;
563+ props . push ( 'node.identifier' ) ;
564+ const q = `CALL db.index.fulltext.queryNodes("${ indexName } ", "${ searchText } ") YIELD node, score RETURN ${ props . join ( ',' ) } , score limit ${ count } ` ;
565+ const queryResult = await this . query ( q ) ;
566+ return queryResult ? queryResult . records . map ( v => {
567+ const result = { } ;
568+ fullTextIndex . properties . forEach ( p => {
569+ result [ p ] = v . get ( `node.${ p } ` )
570+ } ) ;
571+ result [ 'score' ] = v . get ( 'score' ) ;
572+ result [ 'identifier' ] = v . get ( 'node.identifier' ) ;
573+ return result ;
574+ } ) : [ ] ;
575+ }
576+ catch ( err ) {
577+ this . options . logger ?. log ( ( err as object ) . toString ( ) ) ;
578+ throw err ;
579+ }
580+ }
581+
462582 private async validateAndTransformProperties ( transaction , decl : ClassDeclaration , properties : PropertyBag ) : Promise < PropertyBag > {
463583 const factory = new Factory ( this . modelManager ) ;
464584 const serializer = new Serializer ( factory , this . modelManager ) ;
0 commit comments