├── LICENSE ├── LongTermMemory.yml ├── README.md ├── agent-prompt.md └── screenshots ├── LongTermMemory-chatbot-demo-config-tool-node-parameters.png ├── LongTermMemory-chatbot-demo-test-long-term-memory-remembering.png ├── LongTermMemory-chatbot-demo-test-long-term-memory-retrieving.png ├── LongTermMemory-chatbot-demo-test-long-term-memory-standalone-space.png └── LongTermMemory-workflow.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rain Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LongTermMemory.yml: -------------------------------------------------------------------------------- 1 | app: 2 | description: '' 3 | icon: "\U0001F916" 4 | icon_background: '#FFEAD5' 5 | mode: workflow 6 | name: LongTermMemory 7 | workflow: 8 | features: 9 | file_upload: 10 | image: 11 | enabled: false 12 | number_limits: 3 13 | transfer_methods: 14 | - local_file 15 | - remote_url 16 | opening_statement: '' 17 | retriever_resource: 18 | enabled: false 19 | sensitive_word_avoidance: 20 | enabled: false 21 | speech_to_text: 22 | enabled: false 23 | suggested_questions: [] 24 | suggested_questions_after_answer: 25 | enabled: false 26 | text_to_speech: 27 | enabled: false 28 | language: '' 29 | voice: '' 30 | graph: 31 | edges: 32 | - data: 33 | isInIteration: false 34 | sourceType: start 35 | targetType: template-transform 36 | id: 1719906575066-source-1719906601173-target 37 | source: '1719906575066' 38 | sourceHandle: source 39 | target: '1719906601173' 40 | targetHandle: target 41 | type: custom 42 | zIndex: 0 43 | - data: 44 | isInIteration: false 45 | sourceType: template-transform 46 | targetType: code 47 | id: 1719906601173-source-1719906754157-target 48 | source: '1719906601173' 49 | sourceHandle: source 50 | target: '1719906754157' 51 | targetHandle: target 52 | type: custom 53 | zIndex: 0 54 | - data: 55 | isInIteration: false 56 | sourceType: code 57 | targetType: http-request 58 | id: 1719906754157-source-1719906843290-target 59 | source: '1719906754157' 60 | sourceHandle: source 61 | target: '1719906843290' 62 | targetHandle: target 63 | type: custom 64 | zIndex: 0 65 | - data: 66 | isInIteration: false 67 | sourceType: http-request 68 | targetType: code 69 | id: 1719906843290-source-1719906924365-target 70 | source: '1719906843290' 71 | sourceHandle: source 72 | target: '1719906924365' 73 | targetHandle: target 74 | type: custom 75 | zIndex: 0 76 | - data: 77 | isInIteration: false 78 | sourceType: code 79 | targetType: template-transform 80 | id: 1719906924365-source-1719907030231-target 81 | source: '1719906924365' 82 | sourceHandle: source 83 | target: '1719907030231' 84 | targetHandle: target 85 | type: custom 86 | zIndex: 0 87 | - data: 88 | isInIteration: false 89 | sourceType: template-transform 90 | targetType: parameter-extractor 91 | id: 1719907030231-source-1719907179295-target 92 | source: '1719907030231' 93 | sourceHandle: source 94 | target: '1719907179295' 95 | targetHandle: target 96 | type: custom 97 | zIndex: 0 98 | - data: 99 | isInIteration: false 100 | sourceType: parameter-extractor 101 | targetType: if-else 102 | id: 1719907179295-source-1719907351349-target 103 | source: '1719907179295' 104 | sourceHandle: source 105 | target: '1719907351349' 106 | targetHandle: target 107 | type: custom 108 | zIndex: 0 109 | - data: 110 | isInIteration: false 111 | sourceType: if-else 112 | targetType: template-transform 113 | id: 1719907351349-true-1719907410938-target 114 | source: '1719907351349' 115 | sourceHandle: 'true' 116 | target: '1719907410938' 117 | targetHandle: target 118 | type: custom 119 | zIndex: 0 120 | - data: 121 | isInIteration: false 122 | sourceType: if-else 123 | targetType: if-else 124 | id: 1719907351349-false-1719907452578-target 125 | source: '1719907351349' 126 | sourceHandle: 'false' 127 | target: '1719907452578' 128 | targetHandle: target 129 | type: custom 130 | zIndex: 0 131 | - data: 132 | isInIteration: false 133 | sourceType: if-else 134 | targetType: http-request 135 | id: 1719907452578-true-1719907482868-target 136 | source: '1719907452578' 137 | sourceHandle: 'true' 138 | target: '1719907482868' 139 | targetHandle: target 140 | type: custom 141 | zIndex: 0 142 | - data: 143 | isInIteration: false 144 | sourceType: if-else 145 | targetType: http-request 146 | id: 1719907452578-false-1719907683628-target 147 | source: '1719907452578' 148 | sourceHandle: 'false' 149 | target: '1719907683628' 150 | targetHandle: target 151 | type: custom 152 | zIndex: 0 153 | - data: 154 | isInIteration: false 155 | sourceType: template-transform 156 | targetType: variable-aggregator 157 | id: 1719907410938-source-1719907835909-target 158 | selected: false 159 | source: '1719907410938' 160 | sourceHandle: source 161 | target: '1719907835909' 162 | targetHandle: target 163 | type: custom 164 | zIndex: 0 165 | - data: 166 | isInIteration: false 167 | sourceType: http-request 168 | targetType: variable-aggregator 169 | id: 1719907482868-source-1719907835909-target 170 | source: '1719907482868' 171 | sourceHandle: source 172 | target: '1719907835909' 173 | targetHandle: target 174 | type: custom 175 | zIndex: 0 176 | - data: 177 | isInIteration: false 178 | sourceType: http-request 179 | targetType: variable-aggregator 180 | id: 1719907683628-source-1719907835909-target 181 | source: '1719907683628' 182 | sourceHandle: source 183 | target: '1719907835909' 184 | targetHandle: target 185 | type: custom 186 | zIndex: 0 187 | - data: 188 | isInIteration: false 189 | sourceType: variable-aggregator 190 | targetType: end 191 | id: 1719907835909-source-1719906705933-target 192 | source: '1719907835909' 193 | sourceHandle: source 194 | target: '1719906705933' 195 | targetHandle: target 196 | type: custom 197 | zIndex: 0 198 | nodes: 199 | - data: 200 | desc: '' 201 | selected: false 202 | title: Start 203 | type: start 204 | variables: 205 | - label: base_url 206 | max_length: 48 207 | options: [] 208 | required: true 209 | type: text-input 210 | variable: base_url 211 | - label: dataset_id 212 | max_length: 48 213 | options: [] 214 | required: true 215 | type: text-input 216 | variable: dataset_id 217 | - label: document_id 218 | max_length: 48 219 | options: [] 220 | required: true 221 | type: text-input 222 | variable: document_id 223 | - label: api_key 224 | max_length: 48 225 | options: [] 226 | required: true 227 | type: text-input 228 | variable: api_key 229 | - label: memory_template 230 | max_length: 480 231 | options: [] 232 | required: true 233 | type: paragraph 234 | variable: memory_template 235 | - label: user_input 236 | max_length: 2000 237 | options: [] 238 | required: true 239 | type: paragraph 240 | variable: user_input 241 | height: 218 242 | id: '1719906575066' 243 | position: 244 | x: 30 245 | y: 309 246 | positionAbsolute: 247 | x: 30 248 | y: 309 249 | selected: false 250 | sourcePosition: right 251 | targetPosition: left 252 | type: custom 253 | width: 243 254 | - data: 255 | desc: config what kind of content should be remember 256 | selected: false 257 | template: ' 258 | 259 | {{ memory_template }} 260 | 261 | ' 262 | title: Config:MemoryContentTemplate 263 | type: template-transform 264 | variables: 265 | - value_selector: 266 | - sys 267 | - user_id 268 | variable: user_id 269 | - value_selector: 270 | - '1719906575066' 271 | - memory_template 272 | variable: memory_template 273 | height: 100 274 | id: '1719906601173' 275 | position: 276 | x: 333 277 | y: 309 278 | positionAbsolute: 279 | x: 333 280 | y: 309 281 | selected: false 282 | sourcePosition: right 283 | targetPosition: left 284 | type: custom 285 | width: 243 286 | - data: 287 | desc: '' 288 | outputs: 289 | - value_selector: 290 | - '1719906924365' 291 | - longterm_memory_space 292 | variable: longterm_memory_space 293 | selected: false 294 | title: End 295 | type: end 296 | height: 88 297 | id: '1719906705933' 298 | position: 299 | x: 3363 300 | y: 420.5 301 | positionAbsolute: 302 | x: 3363 303 | y: 420.5 304 | selected: false 305 | sourcePosition: right 306 | targetPosition: left 307 | type: custom 308 | width: 243 309 | - data: 310 | code: "def main(base_url: str, dataset_id: str, document_id: str, api_key:\ 311 | \ str) -> dict:\n config = {\n \"base_url\": base_url,\n \ 312 | \ \"dataset_id\": dataset_id,\n \"document_id\": document_id,\n\ 313 | \ \"api_key\": api_key\n }\n\n api_authorization = \"Bearer\ 314 | \ %s\" % config[\"api_key\"]\n url_for_search_segment = \"{config[base_url]}/v1/datasets/{config[dataset_id]}/documents/{config[document_id]}/segments\"\ 315 | .format(config=config)\n \n return {\n \"config\": config,\n\ 316 | \ \"api_authorization\": api_authorization,\n \"url_for_search_segment\"\ 317 | : url_for_search_segment,\n }\n" 318 | code_language: python3 319 | desc: Knowledge config for storing long term memory 320 | outputs: 321 | api_authorization: 322 | children: null 323 | type: string 324 | config: 325 | children: null 326 | type: object 327 | url_for_search_segment: 328 | children: null 329 | type: string 330 | selected: false 331 | title: Config:KB 332 | type: code 333 | variables: 334 | - value_selector: 335 | - '1719906575066' 336 | - base_url 337 | variable: base_url 338 | - value_selector: 339 | - '1719906575066' 340 | - dataset_id 341 | variable: dataset_id 342 | - value_selector: 343 | - '1719906575066' 344 | - document_id 345 | variable: document_id 346 | - value_selector: 347 | - '1719906575066' 348 | - api_key 349 | variable: api_key 350 | height: 100 351 | id: '1719906754157' 352 | position: 353 | x: 636 354 | y: 309 355 | positionAbsolute: 356 | x: 636 357 | y: 309 358 | selected: false 359 | sourcePosition: right 360 | targetPosition: left 361 | type: custom 362 | width: 243 363 | - data: 364 | authorization: 365 | config: null 366 | type: no-auth 367 | body: 368 | data: '' 369 | type: none 370 | desc: retrieve memory of user by searching KB by user id 371 | headers: Authorization:{{#1719906754157.api_authorization#}} 372 | method: get 373 | params: keyword:{{#sys.user_id#}} 374 | selected: false 375 | timeout: 376 | max_connect_timeout: 0 377 | max_read_timeout: 0 378 | max_write_timeout: 0 379 | title: Action:retrieve_memory_of_user 380 | type: http-request 381 | url: '{{#1719906754157.url_for_search_segment#}}' 382 | variables: [] 383 | height: 139 384 | id: '1719906843290' 385 | position: 386 | x: 939 387 | y: 309 388 | positionAbsolute: 389 | x: 939 390 | y: 309 391 | selected: false 392 | sourcePosition: right 393 | targetPosition: left 394 | type: custom 395 | width: 243 396 | - data: 397 | code: "import json\n\ndef main(api_response: str, config: object) -> dict:\n\ 398 | \ url_for_add_segment = \"{config[base_url]}/v1/datasets/{config[dataset_id]}/documents/{config[document_id]}/segments\"\ 399 | .format(config=config)\n url_for_update_segment = \"\"\n longterm_memory_space\ 400 | \ = \"\"\n parsed_response = json.loads(api_response)\n contents =\ 401 | \ [item['content'] for item in parsed_response['data']]\n if contents:\n\ 402 | \ longterm_memory_space = \"\\n\".join(contents)\n # expose\ 403 | \ segment_id\n if len(parsed_response['data']):\n segment_id\ 404 | \ = parsed_response['data'][0]['id']\n longterm_memory_space\ 405 | \ = \"\" + segment_id + \"\\n\" + longterm_memory_space\n\ 406 | \ url_for_update_segment = \"{config[base_url]}/v1/datasets/{config[dataset_id]}/documents/{config[document_id]}/segments/{segment_id}\"\ 407 | .format(config=config, segment_id=segment_id)\n\n return {\n \"\ 408 | longterm_memory_space\": longterm_memory_space,\n \"url_for_add_segment\"\ 409 | : url_for_add_segment,\n \"url_for_update_segment\": url_for_update_segment\n\ 410 | \ }" 411 | code_language: python3 412 | desc: extract content field from KB API response 413 | outputs: 414 | longterm_memory_space: 415 | children: null 416 | type: string 417 | url_for_add_segment: 418 | children: null 419 | type: string 420 | url_for_update_segment: 421 | children: null 422 | type: string 423 | selected: false 424 | title: Code:extract_content_from_api 425 | type: code 426 | variables: 427 | - value_selector: 428 | - '1719906843290' 429 | - body 430 | variable: api_response 431 | - value_selector: 432 | - '1719906754157' 433 | - config 434 | variable: config 435 | height: 100 436 | id: '1719906924365' 437 | position: 438 | x: 1242 439 | y: 309 440 | positionAbsolute: 441 | x: 1242 442 | y: 309 443 | selected: false 444 | sourcePosition: right 445 | targetPosition: left 446 | type: custom 447 | width: 243 448 | - data: 449 | desc: 'prepare agent system prompt 450 | 451 | ' 452 | selected: false 453 | template: "# Long-term Memory Capability\n\nYou possess long-term memory capabilities,\ 454 | \ maintaining an independent long-term memory space for each user you interact\ 455 | \ with. Below is an explanation of long-term memory capabilities:\n\nThere\ 456 | \ is a \"Long-term Memory Content Template\" that declares the attribute\ 457 | \ tags to be saved. When the user's input content is related to the attribute\ 458 | \ tags defined in the template, the system analyzes the user's input based\ 459 | \ on the semantic meaning of the attribute tags in the template, extracts\ 460 | \ the corresponding attribute tag values, and finally generates the content\ 461 | \ for the \"Long-term Memory Space\" according to the template format.\n\ 462 | \n## Long-term Memory Content Template\n\nThe following is the specific\ 463 | \ long-term memory content template, contained within the ``\ 464 | \ tag:\n\n\n{{ longterm_memory_template }}\n\n\ 465 | \n## Long-term Memory Content Template Example\n\nFor example, if the \"\ 466 | Long-term Memory Content Template\" is set as:\n\n```\n\n\n\t${user_name}\n\t${user_age}\n\ 468 | \t${user_hobbies}\n\n\n```\n\ 469 | \nThe defined attribute tags in this example are: ``, ``, ``\n\ 470 | \nExamples of attribute tags corresponding to automatically extracted values\ 471 | \ are:\n\n- The `` attribute tag identifies name-related information\ 472 | \ through the word `name`.\n- The `` attribute tag identifies age-related\ 473 | \ information through the word `age`.\n- The `` attribute tag identifies\ 474 | \ personal interests and hobbies-related information through the word `hobbies`.\n\ 475 | \nExamples of extracting attribute tag values:\n\n- For instance, if the\ 476 | \ user input is `I'm Tom` or `my name is Tom`, the analysis reveals that\ 477 | \ the user's name is `Tom`, so the value of the `` attribute tag is\ 478 | \ `Tom`.\n- If the user input is `I'm 30`, the analysis shows that the user's\ 479 | \ age is `30`, so the value of the `` attribute tag is `30`.\n- If\ 480 | \ the user input is `I like reading`, the analysis indicates that the user's\ 481 | \ hobby is `reading`, so the value of the `` attribute tag is `reading`.\n\ 482 | \nThus, in this example, the final \"Long-term Memory Space\" content generated\ 483 | \ would be:\n\n```\n\n\n\ 484 | \tTom\n\t30\n\treading\n\n\ 485 | \n```\n\n## Long-term Memory Space\n\nThe content of the long-term\ 486 | \ memory space is contained within the ``\ 487 | \ tags:\n\n\n{{ longterm_memory_space }}\n\n\ 488 | \n## Parameter Extraction\n\n### Extracting the `action` Parameter\n\nThe\ 489 | \ following are the conditions for the `action` parameter values (based\ 490 | \ on the `Long-term Memory Content Template` and the `Long-term Memory Content\ 491 | \ Template Example`):\n\n- `add_user_memory`:\n When the `input_text` mentions\ 492 | \ information related to the attribute tags defined in the `Long-term Memory\ 493 | \ Content Template`, and the ``\ 494 | \ tags are **empty**.\n- `update_user_memory`:\n When the `input_text`\ 495 | \ mentions information related to the attribute tags defined in the `Long-term\ 496 | \ Memory Content Template`, and the ``\ 497 | \ tags are **not empty**.\n- `reply`:\n When the `input_text` is **unrelated**\ 498 | \ to the attribute tags defined in the `Long-term Memory Content Template`.\n\ 499 | \n### Extracting the `longterm_memory_content` Parameter\n\nWhen the `action`\ 500 | \ parameter value is not `reply`, refer to the examples described in the\ 501 | \ `Long-term Memory Content Template Example`, use the template defined\ 502 | \ in the `Long-term Memory Content Template`, analyze the user's input,\ 503 | \ combine it with the existing information in the \"Long-term Memory Space\"\ 504 | , extract the values of the attribute tags to generate new \"Long-term Memory\ 505 | \ Space\" content (ensure the generated content structure is the same as\ 506 | \ described in the template and is in XML format), and finally convert the\ 507 | \ long-term memory content into a format suitable for use as a value in\ 508 | \ JSON key-value pairs (ensure illegal characters are escaped).\n\n" 509 | title: Agent prompt 510 | type: template-transform 511 | variables: 512 | - value_selector: 513 | - '1719906601173' 514 | - output 515 | variable: longterm_memory_template 516 | - value_selector: 517 | - '1719906924365' 518 | - longterm_memory_space 519 | variable: longterm_memory_space 520 | - value_selector: 521 | - sys 522 | - user_id 523 | variable: user_id 524 | height: 82 525 | id: '1719907030231' 526 | position: 527 | x: 1545 528 | y: 309 529 | positionAbsolute: 530 | x: 1545 531 | y: 309 532 | selected: false 533 | sourcePosition: right 534 | targetPosition: left 535 | type: custom 536 | width: 243 537 | - data: 538 | desc: use LLM to extract action and longterm_memory_content 539 | instruction: '{{#1719907030231.output#}}' 540 | model: 541 | completion_params: 542 | temperature: 0.7 543 | mode: chat 544 | name: deepseek-chat 545 | provider: deepseek 546 | parameters: 547 | - description: 'action to take, should be one of: add_user_memory,update_user_memory,reply' 548 | name: action 549 | required: true 550 | type: string 551 | - description: longterm memory content 552 | name: longterm_memory_content 553 | required: false 554 | type: string 555 | query: 556 | - '1719906575066' 557 | - user_input 558 | reasoning_mode: prompt 559 | selected: false 560 | title: LLM:Parameter Extractor 561 | type: parameter-extractor 562 | variables: [] 563 | height: 144 564 | id: '1719907179295' 565 | position: 566 | x: 1848 567 | y: 309 568 | positionAbsolute: 569 | x: 1848 570 | y: 309 571 | selected: false 572 | sourcePosition: right 573 | targetPosition: left 574 | type: custom 575 | width: 243 576 | - data: 577 | conditions: 578 | - comparison_operator: is 579 | id: '1719907361059' 580 | value: reply 581 | variable_selector: 582 | - '1719907179295' 583 | - action 584 | desc: '' 585 | logical_operator: and 586 | selected: false 587 | title: IF:action is reply 588 | type: if-else 589 | height: 124 590 | id: '1719907351349' 591 | position: 592 | x: 2151 593 | y: 309 594 | positionAbsolute: 595 | x: 2151 596 | y: 309 597 | sourcePosition: right 598 | targetPosition: left 599 | type: custom 600 | width: 243 601 | - data: 602 | desc: '' 603 | selected: false 604 | template: just answer user query 605 | title: Action:reply 606 | type: template-transform 607 | variables: 608 | - value_selector: 609 | - '1719907179295' 610 | - action 611 | variable: action 612 | height: 52 613 | id: '1719907410938' 614 | position: 615 | x: 2757 616 | y: 309 617 | positionAbsolute: 618 | x: 2757 619 | y: 309 620 | selected: false 621 | sourcePosition: right 622 | targetPosition: left 623 | type: custom 624 | width: 243 625 | - data: 626 | conditions: 627 | - comparison_operator: is 628 | id: '1719907463190' 629 | value: add_user_memory 630 | variable_selector: 631 | - '1719907179295' 632 | - action 633 | desc: '' 634 | logical_operator: and 635 | selected: false 636 | title: IF:action is add_user_memory 637 | type: if-else 638 | height: 124 639 | id: '1719907452578' 640 | position: 641 | x: 2454 642 | y: 420.5 643 | positionAbsolute: 644 | x: 2454 645 | y: 420.5 646 | selected: false 647 | sourcePosition: right 648 | targetPosition: left 649 | type: custom 650 | width: 243 651 | - data: 652 | authorization: 653 | config: null 654 | type: no-auth 655 | body: 656 | data: '{"segments": [{"content": "{{#1719907179295.longterm_memory_content#}}","keywords": 657 | ["{{#sys.user_id#}}"]}]}' 658 | type: json 659 | desc: '' 660 | headers: Authorization:{{#1719906754157.api_authorization#}} 661 | method: post 662 | params: '' 663 | selected: false 664 | timeout: 665 | max_connect_timeout: 0 666 | max_read_timeout: 0 667 | max_write_timeout: 0 668 | title: Action:add_memory 669 | type: http-request 670 | url: '{{#1719906924365.url_for_add_segment#}}' 671 | variables: [] 672 | height: 91 673 | id: '1719907482868' 674 | position: 675 | x: 2757 676 | y: 401 677 | positionAbsolute: 678 | x: 2757 679 | y: 401 680 | selected: false 681 | sourcePosition: right 682 | targetPosition: left 683 | type: custom 684 | width: 243 685 | - data: 686 | authorization: 687 | config: null 688 | type: no-auth 689 | body: 690 | data: '{"segment": {"content": "{{#1719907179295.longterm_memory_content#}}", 691 | "keywords": ["{{#sys.user_id#}}"]}}' 692 | type: json 693 | desc: '' 694 | headers: Authorization:{{#1719906754157.api_authorization#}} 695 | method: post 696 | params: '' 697 | selected: false 698 | timeout: 699 | max_connect_timeout: 0 700 | max_read_timeout: 0 701 | max_write_timeout: 0 702 | title: Action:update_memory 703 | type: http-request 704 | url: '{{#1719906924365.url_for_update_segment#}}' 705 | variables: [] 706 | height: 91 707 | id: '1719907683628' 708 | position: 709 | x: 2757 710 | y: 532 711 | positionAbsolute: 712 | x: 2757 713 | y: 532 714 | selected: false 715 | sourcePosition: right 716 | targetPosition: left 717 | type: custom 718 | width: 243 719 | - data: 720 | advanced_settings: 721 | group_enabled: true 722 | groups: 723 | - groupId: 140ac543-c56b-4500-a03d-a8b73903a3a5 724 | group_name: action_result 725 | output_type: string 726 | variables: 727 | - - '1719907410938' 728 | - output 729 | - - '1719907482868' 730 | - body 731 | - - '1719907683628' 732 | - body 733 | desc: '' 734 | output_type: string 735 | selected: false 736 | title: Variable Aggregator 737 | type: variable-aggregator 738 | variables: 739 | - - '1719907410938' 740 | - output 741 | height: 163 742 | id: '1719907835909' 743 | position: 744 | x: 3060 745 | y: 420.5 746 | positionAbsolute: 747 | x: 3060 748 | y: 420.5 749 | selected: false 750 | sourcePosition: right 751 | targetPosition: left 752 | type: custom 753 | width: 243 754 | - data: 755 | author: admin 756 | desc: '' 757 | height: 114 758 | selected: false 759 | showAuthor: false 760 | text: '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"important 761 | note:","type":"text","version":1},{"type":"linebreak","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"MODEL 762 | for `","type":"text","version":1},{"detail":0,"format":16,"mode":"normal","style":"","text":"LLM:Parameter 763 | Extractor","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" 764 | node`, LLM performance must be >= gpt4","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}' 765 | theme: blue 766 | title: '' 767 | type: '' 768 | width: 251 769 | height: 114 770 | id: '1719920303831' 771 | position: 772 | x: 1848 773 | y: 480.37757179352536 774 | positionAbsolute: 775 | x: 1848 776 | y: 480.37757179352536 777 | selected: false 778 | sourcePosition: right 779 | targetPosition: left 780 | type: custom-note 781 | width: 251 782 | - data: 783 | author: admin 784 | desc: '' 785 | height: 177 786 | selected: false 787 | showAuthor: false 788 | text: '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"memory_template 789 | example:","type":"text","version":1},{"type":"linebreak","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":" ${user_name}","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":" ${user_age}","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":" ${user_hobbies}","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":" ${any_user_info}","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}' 790 | theme: blue 791 | title: '' 792 | type: '' 793 | width: 303 794 | height: 177 795 | id: '1719920815505' 796 | position: 797 | x: 333 798 | y: 445.7167598179409 799 | positionAbsolute: 800 | x: 333 801 | y: 445.7167598179409 802 | selected: true 803 | sourcePosition: right 804 | targetPosition: left 805 | type: custom-note 806 | width: 303 807 | viewport: 808 | x: 143 809 | y: 35 810 | zoom: 0.7 811 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About this repo 2 | 3 | a Dify tool for storing and retrieving long-term-memory, using Dify built-in Knowledge dataset for storing memories, each user has a standalone long-term-memory space. 4 | Make chatbot can persistently remember customized information per user. 5 | 6 | workflow view: 7 | 8 | ![LongTermMemory-workflow](screenshots/LongTermMemory-workflow.png) 9 | 10 | # How it works 11 | 12 | Here is the major logic of this workflow: 13 | 14 | 1. before each conversation run a "HTTP Request" node named "Action:retrieve_memory_of_user" to request Dify Knowledge API to query user related document segments with filter parameters as `keyword=$current_user_id` 15 | 2. run a "Template" node as "Agent prompt"([agent prompt content](agent-prompt.md)) for preparing agent system prompt, injected customized `$memory_template` 16 | 17 | 2. run a "Parameter Extractor" node to extract `$action`(value will be one of reply/add_user_memory/update_user_memory ) and `$longterm_memory_content` parameters 18 | 3. if the `$action` is not "reply", run a "HTTP Request" node to request Dify Knowledge API to create or update the document segment with `$longterm_memory_content` parameter 19 | 4. if the `$action` is "reply", return the retrieved segments as a output variable `$longterm_memory_space`, so that it can be injected as a LLM node's context in a chatbot 20 | 21 | 22 | # Demo Video 23 | 24 | https://github.com/user-attachments/assets/a61c065f-5e74-4ea3-95ab-5badc87bc9a1 25 | 26 | 27 | # Usage Steps 28 | 29 | ## step 1: create a Knowledge and document for storing long-term-memory 30 | 31 | create a new Knowledge and import an empty document 32 | 33 | - document name: whatever 34 | 35 | - Chunk settings: Automatic 36 | 37 | - Index mode: Economical 38 | 39 | ## step 2: Create a new Secret key for accessing Knowledge API 40 | 41 | 42 | 43 | ## step 3: import the workflow DSL yaml 44 | 45 | 1) download [LongTermMemory.yml](https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/main/LongTermMemory.yml) file and import it using Dify "Import DSL file" 46 | 47 | 2) In the imported workflow, config MODEL for `LLM:Parameter Extractor` node, which LLM performance must be >= gpt4. 48 | Tips: gpt4 series, deepseek-chat are recommended, gpt3.5-turbo was tested but not powerful enough to run this tool. 49 | 50 | 51 | ## step 4: publish the workflow as tool "LongTermMemory" 52 | 53 | 54 | 55 | ## step 5: create new a chatbot app using chatflow 56 | 57 | 58 | 59 | ## step 6: add the "LongTermMemory" tool 60 | 61 | add the "LongTermMemory" tool before LLM node 62 | 63 | 64 | 65 | ## step 7: config parameters for "LongTermMemory" tool 66 | 67 | ![LongTermMemory-chatbot-demo-config-tool-node-parameters](screenshots/LongTermMemory-chatbot-demo-config-tool-node-parameters.png) 68 | 69 | notes for parameters: 70 | 71 | - base_url: `http://host.docker.internal` for docker-compose deployment on MacOS(use `http://docker.for.win.localhost` for Windows 10/11), append the port like `:8082` if not using default nginx port config 72 | 73 | - dataset_id: dataset id 74 | 75 | - document_id: document id 76 | 77 | - api_key: Dify Knowledge API key, this is not "Chat App API key". You can get one on 'Knowledge -> API ACCESS' page, a Knowledge API key should be look like "dataset-xxx" 78 | 79 | - memory_template: for example 80 | 81 | ``` 82 | 83 | ${user_name} 84 | ${user_age} 85 | ${user_hobbies} 86 | ${any_user_info} 87 | 88 | ``` 89 | 90 | - user_input: `{{#sys.query#}}` 91 | 92 | 93 | 94 | ## step 8: Config LLM node 95 | 96 | 97 | 98 | CONTEXT:select `LongTermMemory` node's `text` 99 | 100 | SYSTEM prompt: 101 | 102 | ``` 103 | {{#context#}} 104 | ``` 105 | 106 | 107 | 108 | # Test: 109 | 110 | ### Scenario 1: test long-term-memory remembering 111 | 112 | ![LongTermMemory-chatbot-demo-test-long-term-memory remembering](screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-remembering.png) 113 | 114 | user input: 115 | 116 | - > who am I? 117 | 118 | - chatbot should reply no idea 119 | 120 | - > I'm Rain 121 | 122 | - check out the knowledge page, there should be a new segment with user_id, name, and keyword attribute tags 123 | 124 | - > I like coding 125 | 126 | - check out the knowledge page, the segment should be updated, tag should be updated 127 | 128 | - > I like reading, also 129 | 130 | - check out the knowledge page, the segment should be updated, tag should be updated 131 | 132 | ### Scenario 2: test long-term-memory retrieving 133 | 134 | ![LongTermMemory-chatbot-demo-test-long-term-memory-retrieving](screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-retrieving.png) 135 | 136 | Debug and Preview -> Restart (make sure to start a new conversion without chat history) 137 | 138 | user input: 139 | 140 | - what's my name? 141 | - chatbot should reply current user's name 142 | - what do I like? 143 | - chatbot should reply current user's hobbies 144 | 145 | 146 | 147 | ### Scenario 3: each user has a standalone long-term-memory space 148 | 149 | ![LongTermMemory-chatbot-demo-test-long-term-memory-standalone-space](screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-standalone-space.png) 150 | 151 | Run App (You can use different browsers to access and impersonate different users) 152 | 153 | user input: 154 | 155 | - I'm Jerry 156 | - I like eating 157 | - check out the knowledge page, there should be a new segment with different user_id, name, and keyword attribute tags 158 | -------------------------------------------------------------------------------- /agent-prompt.md: -------------------------------------------------------------------------------- 1 | # Long-term Memory Capability 2 | 3 | You possess long-term memory capabilities, maintaining an independent long-term memory space for each user you interact with. Below is an explanation of long-term memory capabilities: 4 | 5 | There is a "Long-term Memory Content Template" that declares the attribute tags to be saved. When the user's input content is related to the attribute tags defined in the template, the system analyzes the user's input based on the semantic meaning of the attribute tags in the template, extracts the corresponding attribute tag values, and finally generates the content for the "Long-term Memory Space" according to the template format. 6 | 7 | ## Long-term Memory Content Template 8 | 9 | The following is the specific long-term memory content template, contained within the `` tag: 10 | 11 | 12 | {{ longterm_memory_template }} 13 | 14 | 15 | ## Long-term Memory Content Template Example 16 | 17 | For example, if the "Long-term Memory Content Template" is set as: 18 | 19 | ``` 20 | 21 | 22 | ${user_name} 23 | ${user_age} 24 | ${user_hobbies} 25 | 26 | 27 | ``` 28 | 29 | The defined attribute tags in this example are: ``, ``, `` 30 | 31 | Examples of attribute tags corresponding to automatically extracted values are: 32 | 33 | - The `` attribute tag identifies name-related information through the word `name`. 34 | - The `` attribute tag identifies age-related information through the word `age`. 35 | - The `` attribute tag identifies personal interests and hobbies-related information through the word `hobbies`. 36 | 37 | Examples of extracting attribute tag values: 38 | 39 | - For instance, if the user input is `I'm Tom` or `my name is Tom`, the analysis reveals that the user's name is `Tom`, so the value of the `` attribute tag is `Tom`. 40 | - If the user input is `I'm 30`, the analysis shows that the user's age is `30`, so the value of the `` attribute tag is `30`. 41 | - If the user input is `I like reading`, the analysis indicates that the user's hobby is `reading`, so the value of the `` attribute tag is `reading`. 42 | 43 | Thus, in this example, the final "Long-term Memory Space" content generated would be: 44 | 45 | ``` 46 | 47 | 48 | Tom 49 | 30 50 | reading 51 | 52 | 53 | ``` 54 | 55 | ## Long-term Memory Space 56 | 57 | The content of the long-term memory space is contained within the `` tags: 58 | 59 | 60 | {{ longterm_memory_space }} 61 | 62 | 63 | ## Parameter Extraction 64 | 65 | ### Extracting the `action` Parameter 66 | 67 | The following are the conditions for the `action` parameter values (based on the `Long-term Memory Content Template` and the `Long-term Memory Content Template Example`): 68 | 69 | - `add_user_memory`: 70 | When the `input_text` mentions information related to the attribute tags defined in the `Long-term Memory Content Template`, and the `` tags are **empty**. 71 | - `update_user_memory`: 72 | When the `input_text` mentions information related to the attribute tags defined in the `Long-term Memory Content Template`, and the `` tags are **not empty**. 73 | - `reply`: 74 | When the `input_text` is **unrelated** to the attribute tags defined in the `Long-term Memory Content Template`. 75 | 76 | ### Extracting the `longterm_memory_content` Parameter 77 | 78 | When the `action` parameter value is not `reply`, refer to the examples described in the `Long-term Memory Content Template Example`, use the template defined in the `Long-term Memory Content Template`, analyze the user's input, combine it with the existing information in the "Long-term Memory Space", extract the values of the attribute tags to generate new "Long-term Memory Space" content (ensure the generated content structure is the same as described in the template and is in XML format), and finally convert the long-term memory content into a format suitable for use as a value in JSON key-value pairs (ensure illegal characters are escaped). 79 | 80 | -------------------------------------------------------------------------------- /screenshots/LongTermMemory-chatbot-demo-config-tool-node-parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/85c3b2abca0b8e178b5f326320687fef8c1b1aa4/screenshots/LongTermMemory-chatbot-demo-config-tool-node-parameters.png -------------------------------------------------------------------------------- /screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-remembering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/85c3b2abca0b8e178b5f326320687fef8c1b1aa4/screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-remembering.png -------------------------------------------------------------------------------- /screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-retrieving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/85c3b2abca0b8e178b5f326320687fef8c1b1aa4/screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-retrieving.png -------------------------------------------------------------------------------- /screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-standalone-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/85c3b2abca0b8e178b5f326320687fef8c1b1aa4/screenshots/LongTermMemory-chatbot-demo-test-long-term-memory-standalone-space.png -------------------------------------------------------------------------------- /screenshots/LongTermMemory-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainchen/dify-tool-LongTermMemory/85c3b2abca0b8e178b5f326320687fef8c1b1aa4/screenshots/LongTermMemory-workflow.png --------------------------------------------------------------------------------