diff --git a/LICENSE b/experiencemaker/LICENSE
similarity index 100%
rename from LICENSE
rename to experiencemaker/LICENSE
diff --git a/README.md b/experiencemaker/README.md
similarity index 100%
rename from README.md
rename to experiencemaker/README.md
diff --git a/cookbook/__init__.py b/experiencemaker/cookbook/__init__.py
similarity index 100%
rename from cookbook/__init__.py
rename to experiencemaker/cookbook/__init__.py
diff --git a/cookbook/appworld/__init__.py b/experiencemaker/cookbook/appworld/__init__.py
similarity index 100%
rename from cookbook/appworld/__init__.py
rename to experiencemaker/cookbook/appworld/__init__.py
diff --git a/cookbook/appworld/appworld_react_agent.py b/experiencemaker/cookbook/appworld/appworld_react_agent.py
similarity index 100%
rename from cookbook/appworld/appworld_react_agent.py
rename to experiencemaker/cookbook/appworld/appworld_react_agent.py
diff --git a/cookbook/appworld/prompt.py b/experiencemaker/cookbook/appworld/prompt.py
similarity index 100%
rename from cookbook/appworld/prompt.py
rename to experiencemaker/cookbook/appworld/prompt.py
diff --git a/cookbook/appworld/quickstart.md b/experiencemaker/cookbook/appworld/quickstart.md
similarity index 100%
rename from cookbook/appworld/quickstart.md
rename to experiencemaker/cookbook/appworld/quickstart.md
diff --git a/cookbook/appworld/requirements.txt b/experiencemaker/cookbook/appworld/requirements.txt
similarity index 100%
rename from cookbook/appworld/requirements.txt
rename to experiencemaker/cookbook/appworld/requirements.txt
diff --git a/cookbook/appworld/run_appworld.py b/experiencemaker/cookbook/appworld/run_appworld.py
similarity index 100%
rename from cookbook/appworld/run_appworld.py
rename to experiencemaker/cookbook/appworld/run_appworld.py
diff --git a/cookbook/appworld/run_exp_statistic.py b/experiencemaker/cookbook/appworld/run_exp_statistic.py
similarity index 100%
rename from cookbook/appworld/run_exp_statistic.py
rename to experiencemaker/cookbook/appworld/run_exp_statistic.py
diff --git a/cookbook/bfcl/__init__.py b/experiencemaker/cookbook/bfcl/__init__.py
similarity index 100%
rename from cookbook/bfcl/__init__.py
rename to experiencemaker/cookbook/bfcl/__init__.py
diff --git a/cookbook/frozenlake/__init__.py b/experiencemaker/cookbook/frozenlake/__init__.py
similarity index 100%
rename from cookbook/frozenlake/__init__.py
rename to experiencemaker/cookbook/frozenlake/__init__.py
diff --git a/cookbook/frozenlake/frozenlake_prompts.yaml b/experiencemaker/cookbook/frozenlake/frozenlake_prompts.yaml
similarity index 100%
rename from cookbook/frozenlake/frozenlake_prompts.yaml
rename to experiencemaker/cookbook/frozenlake/frozenlake_prompts.yaml
diff --git a/cookbook/frozenlake/frozenlake_react_agent.py b/experiencemaker/cookbook/frozenlake/frozenlake_react_agent.py
similarity index 100%
rename from cookbook/frozenlake/frozenlake_react_agent.py
rename to experiencemaker/cookbook/frozenlake/frozenlake_react_agent.py
diff --git a/cookbook/frozenlake/map_manager.py b/experiencemaker/cookbook/frozenlake/map_manager.py
similarity index 100%
rename from cookbook/frozenlake/map_manager.py
rename to experiencemaker/cookbook/frozenlake/map_manager.py
diff --git a/cookbook/frozenlake/quickstart.md b/experiencemaker/cookbook/frozenlake/quickstart.md
similarity index 100%
rename from cookbook/frozenlake/quickstart.md
rename to experiencemaker/cookbook/frozenlake/quickstart.md
diff --git a/cookbook/frozenlake/run_exp_statistic.py b/experiencemaker/cookbook/frozenlake/run_exp_statistic.py
similarity index 100%
rename from cookbook/frozenlake/run_exp_statistic.py
rename to experiencemaker/cookbook/frozenlake/run_exp_statistic.py
diff --git a/cookbook/frozenlake/run_frozenlake.py b/experiencemaker/cookbook/frozenlake/run_frozenlake.py
similarity index 100%
rename from cookbook/frozenlake/run_frozenlake.py
rename to experiencemaker/cookbook/frozenlake/run_frozenlake.py
diff --git a/cookbook/simple_demo/__init__.py b/experiencemaker/cookbook/simple_demo/__init__.py
similarity index 100%
rename from cookbook/simple_demo/__init__.py
rename to experiencemaker/cookbook/simple_demo/__init__.py
diff --git a/cookbook/simple_demo/experience.jsonl b/experiencemaker/cookbook/simple_demo/experience.jsonl
similarity index 100%
rename from cookbook/simple_demo/experience.jsonl
rename to experiencemaker/cookbook/simple_demo/experience.jsonl
diff --git a/cookbook/simple_demo/messages.jsonl b/experiencemaker/cookbook/simple_demo/messages.jsonl
similarity index 100%
rename from cookbook/simple_demo/messages.jsonl
rename to experiencemaker/cookbook/simple_demo/messages.jsonl
diff --git a/cookbook/simple_demo/quick_start.md b/experiencemaker/cookbook/simple_demo/quick_start.md
similarity index 100%
rename from cookbook/simple_demo/quick_start.md
rename to experiencemaker/cookbook/simple_demo/quick_start.md
diff --git a/cookbook/simple_demo/simple_demo.py b/experiencemaker/cookbook/simple_demo/simple_demo.py
similarity index 100%
rename from cookbook/simple_demo/simple_demo.py
rename to experiencemaker/cookbook/simple_demo/simple_demo.py
diff --git a/doc/configuration_guide.md b/experiencemaker/doc/configuration_guide.md
similarity index 100%
rename from doc/configuration_guide.md
rename to experiencemaker/doc/configuration_guide.md
diff --git a/doc/figure/framework.png b/experiencemaker/doc/figure/framework.png
similarity index 100%
rename from doc/figure/framework.png
rename to experiencemaker/doc/figure/framework.png
diff --git a/doc/figure/frozenlake_failure.gif b/experiencemaker/doc/figure/frozenlake_failure.gif
similarity index 100%
rename from doc/figure/frozenlake_failure.gif
rename to experiencemaker/doc/figure/frozenlake_failure.gif
diff --git a/doc/figure/frozenlake_success.gif b/experiencemaker/doc/figure/frozenlake_success.gif
similarity index 100%
rename from doc/figure/frozenlake_success.gif
rename to experiencemaker/doc/figure/frozenlake_success.gif
diff --git a/doc/figure/logo.jpg b/experiencemaker/doc/figure/logo.jpg
similarity index 100%
rename from doc/figure/logo.jpg
rename to experiencemaker/doc/figure/logo.jpg
diff --git a/doc/figure/logo.png b/experiencemaker/doc/figure/logo.png
similarity index 100%
rename from doc/figure/logo.png
rename to experiencemaker/doc/figure/logo.png
diff --git a/doc/figure/logo_v2.png b/experiencemaker/doc/figure/logo_v2.png
similarity index 100%
rename from doc/figure/logo_v2.png
rename to experiencemaker/doc/figure/logo_v2.png
diff --git a/doc/figure/logo_v3.jpg b/experiencemaker/doc/figure/logo_v3.jpg
similarity index 100%
rename from doc/figure/logo_v3.jpg
rename to experiencemaker/doc/figure/logo_v3.jpg
diff --git a/doc/future_roadmap.md b/experiencemaker/doc/future_roadmap.md
similarity index 100%
rename from doc/future_roadmap.md
rename to experiencemaker/doc/future_roadmap.md
diff --git a/doc/mcp_quick_start.md b/experiencemaker/doc/mcp_quick_start.md
similarity index 100%
rename from doc/mcp_quick_start.md
rename to experiencemaker/doc/mcp_quick_start.md
diff --git a/doc/operations_documentation.md b/experiencemaker/doc/operations_documentation.md
similarity index 100%
rename from doc/operations_documentation.md
rename to experiencemaker/doc/operations_documentation.md
diff --git a/doc/vector_store_setup.md b/experiencemaker/doc/vector_store_setup.md
similarity index 100%
rename from doc/vector_store_setup.md
rename to experiencemaker/doc/vector_store_setup.md
diff --git a/example.env b/experiencemaker/example.env
similarity index 100%
rename from example.env
rename to experiencemaker/example.env
diff --git a/experiencemaker/__init__.py b/experiencemaker/experiencemaker/__init__.py
similarity index 100%
rename from experiencemaker/__init__.py
rename to experiencemaker/experiencemaker/__init__.py
diff --git a/experiencemaker/app.py b/experiencemaker/experiencemaker/app.py
similarity index 100%
rename from experiencemaker/app.py
rename to experiencemaker/experiencemaker/app.py
diff --git a/experiencemaker/config/__init__.py b/experiencemaker/experiencemaker/config/__init__.py
similarity index 100%
rename from experiencemaker/config/__init__.py
rename to experiencemaker/experiencemaker/config/__init__.py
diff --git a/experiencemaker/config/config_parser.py b/experiencemaker/experiencemaker/config/config_parser.py
similarity index 100%
rename from experiencemaker/config/config_parser.py
rename to experiencemaker/experiencemaker/config/config_parser.py
diff --git a/experiencemaker/config/default_config.yaml b/experiencemaker/experiencemaker/config/default_config.yaml
similarity index 100%
rename from experiencemaker/config/default_config.yaml
rename to experiencemaker/experiencemaker/config/default_config.yaml
diff --git a/experiencemaker/config/mock_config.yaml b/experiencemaker/experiencemaker/config/mock_config.yaml
similarity index 100%
rename from experiencemaker/config/mock_config.yaml
rename to experiencemaker/experiencemaker/config/mock_config.yaml
diff --git a/experiencemaker/config/simple_config.yaml b/experiencemaker/experiencemaker/config/simple_config.yaml
similarity index 100%
rename from experiencemaker/config/simple_config.yaml
rename to experiencemaker/experiencemaker/config/simple_config.yaml
diff --git a/experiencemaker/embedding_model/__init__.py b/experiencemaker/experiencemaker/embedding_model/__init__.py
similarity index 100%
rename from experiencemaker/embedding_model/__init__.py
rename to experiencemaker/experiencemaker/embedding_model/__init__.py
diff --git a/experiencemaker/embedding_model/base_embedding_model.py b/experiencemaker/experiencemaker/embedding_model/base_embedding_model.py
similarity index 100%
rename from experiencemaker/embedding_model/base_embedding_model.py
rename to experiencemaker/experiencemaker/embedding_model/base_embedding_model.py
diff --git a/experiencemaker/embedding_model/openai_compatible_embedding_model.py b/experiencemaker/experiencemaker/embedding_model/openai_compatible_embedding_model.py
similarity index 100%
rename from experiencemaker/embedding_model/openai_compatible_embedding_model.py
rename to experiencemaker/experiencemaker/embedding_model/openai_compatible_embedding_model.py
diff --git a/experiencemaker/enumeration/__init__.py b/experiencemaker/experiencemaker/enumeration/__init__.py
similarity index 100%
rename from experiencemaker/enumeration/__init__.py
rename to experiencemaker/experiencemaker/enumeration/__init__.py
diff --git a/experiencemaker/enumeration/agent_state.py b/experiencemaker/experiencemaker/enumeration/agent_state.py
similarity index 100%
rename from experiencemaker/enumeration/agent_state.py
rename to experiencemaker/experiencemaker/enumeration/agent_state.py
diff --git a/experiencemaker/enumeration/chunk_enum.py b/experiencemaker/experiencemaker/enumeration/chunk_enum.py
similarity index 100%
rename from experiencemaker/enumeration/chunk_enum.py
rename to experiencemaker/experiencemaker/enumeration/chunk_enum.py
diff --git a/experiencemaker/enumeration/http_enum.py b/experiencemaker/experiencemaker/enumeration/http_enum.py
similarity index 100%
rename from experiencemaker/enumeration/http_enum.py
rename to experiencemaker/experiencemaker/enumeration/http_enum.py
diff --git a/experiencemaker/enumeration/role.py b/experiencemaker/experiencemaker/enumeration/role.py
similarity index 100%
rename from experiencemaker/enumeration/role.py
rename to experiencemaker/experiencemaker/enumeration/role.py
diff --git a/experiencemaker/llm/__init__.py b/experiencemaker/experiencemaker/llm/__init__.py
similarity index 100%
rename from experiencemaker/llm/__init__.py
rename to experiencemaker/experiencemaker/llm/__init__.py
diff --git a/experiencemaker/llm/base_llm.py b/experiencemaker/experiencemaker/llm/base_llm.py
similarity index 100%
rename from experiencemaker/llm/base_llm.py
rename to experiencemaker/experiencemaker/llm/base_llm.py
diff --git a/experiencemaker/llm/openai_compatible_llm.py b/experiencemaker/experiencemaker/llm/openai_compatible_llm.py
similarity index 100%
rename from experiencemaker/llm/openai_compatible_llm.py
rename to experiencemaker/experiencemaker/llm/openai_compatible_llm.py
diff --git a/experiencemaker/mcp_server.py b/experiencemaker/experiencemaker/mcp_server.py
similarity index 100%
rename from experiencemaker/mcp_server.py
rename to experiencemaker/experiencemaker/mcp_server.py
diff --git a/experiencemaker/op/__init__.py b/experiencemaker/experiencemaker/op/__init__.py
similarity index 100%
rename from experiencemaker/op/__init__.py
rename to experiencemaker/experiencemaker/op/__init__.py
diff --git a/experiencemaker/op/base_op.py b/experiencemaker/experiencemaker/op/base_op.py
similarity index 100%
rename from experiencemaker/op/base_op.py
rename to experiencemaker/experiencemaker/op/base_op.py
diff --git a/experiencemaker/op/mock_op.py b/experiencemaker/experiencemaker/op/mock_op.py
similarity index 100%
rename from experiencemaker/op/mock_op.py
rename to experiencemaker/experiencemaker/op/mock_op.py
diff --git a/experiencemaker/op/prompt_mixin.py b/experiencemaker/experiencemaker/op/prompt_mixin.py
similarity index 100%
rename from experiencemaker/op/prompt_mixin.py
rename to experiencemaker/experiencemaker/op/prompt_mixin.py
diff --git a/experiencemaker/op/react/__init__.py b/experiencemaker/experiencemaker/op/react/__init__.py
similarity index 100%
rename from experiencemaker/op/react/__init__.py
rename to experiencemaker/experiencemaker/op/react/__init__.py
diff --git a/experiencemaker/op/react/react_v1_op.py b/experiencemaker/experiencemaker/op/react/react_v1_op.py
similarity index 100%
rename from experiencemaker/op/react/react_v1_op.py
rename to experiencemaker/experiencemaker/op/react/react_v1_op.py
diff --git a/experiencemaker/op/react/react_v1_prompt.yaml b/experiencemaker/experiencemaker/op/react/react_v1_prompt.yaml
similarity index 100%
rename from experiencemaker/op/react/react_v1_prompt.yaml
rename to experiencemaker/experiencemaker/op/react/react_v1_prompt.yaml
diff --git a/experiencemaker/op/retriever/__init__.py b/experiencemaker/experiencemaker/op/retriever/__init__.py
similarity index 100%
rename from experiencemaker/op/retriever/__init__.py
rename to experiencemaker/experiencemaker/op/retriever/__init__.py
diff --git a/experiencemaker/op/retriever/build_query_op.py b/experiencemaker/experiencemaker/op/retriever/build_query_op.py
similarity index 100%
rename from experiencemaker/op/retriever/build_query_op.py
rename to experiencemaker/experiencemaker/op/retriever/build_query_op.py
diff --git a/experiencemaker/op/retriever/build_query_prompt.yaml b/experiencemaker/experiencemaker/op/retriever/build_query_prompt.yaml
similarity index 100%
rename from experiencemaker/op/retriever/build_query_prompt.yaml
rename to experiencemaker/experiencemaker/op/retriever/build_query_prompt.yaml
diff --git a/experiencemaker/op/retriever/merge_experience_op.py b/experiencemaker/experiencemaker/op/retriever/merge_experience_op.py
similarity index 100%
rename from experiencemaker/op/retriever/merge_experience_op.py
rename to experiencemaker/experiencemaker/op/retriever/merge_experience_op.py
diff --git a/experiencemaker/op/retriever/rerank_experience_op.py b/experiencemaker/experiencemaker/op/retriever/rerank_experience_op.py
similarity index 100%
rename from experiencemaker/op/retriever/rerank_experience_op.py
rename to experiencemaker/experiencemaker/op/retriever/rerank_experience_op.py
diff --git a/experiencemaker/op/retriever/rerank_experience_prompt.yaml b/experiencemaker/experiencemaker/op/retriever/rerank_experience_prompt.yaml
similarity index 100%
rename from experiencemaker/op/retriever/rerank_experience_prompt.yaml
rename to experiencemaker/experiencemaker/op/retriever/rerank_experience_prompt.yaml
diff --git a/experiencemaker/op/retriever/rewrite_experience_op.py b/experiencemaker/experiencemaker/op/retriever/rewrite_experience_op.py
similarity index 100%
rename from experiencemaker/op/retriever/rewrite_experience_op.py
rename to experiencemaker/experiencemaker/op/retriever/rewrite_experience_op.py
diff --git a/experiencemaker/op/retriever/rewrite_experience_prompt.yaml b/experiencemaker/experiencemaker/op/retriever/rewrite_experience_prompt.yaml
similarity index 100%
rename from experiencemaker/op/retriever/rewrite_experience_prompt.yaml
rename to experiencemaker/experiencemaker/op/retriever/rewrite_experience_prompt.yaml
diff --git a/experiencemaker/op/summarizer/__init__.py b/experiencemaker/experiencemaker/op/summarizer/__init__.py
similarity index 100%
rename from experiencemaker/op/summarizer/__init__.py
rename to experiencemaker/experiencemaker/op/summarizer/__init__.py
diff --git a/experiencemaker/op/summarizer/comparative_extraction_op.py b/experiencemaker/experiencemaker/op/summarizer/comparative_extraction_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/comparative_extraction_op.py
rename to experiencemaker/experiencemaker/op/summarizer/comparative_extraction_op.py
diff --git a/experiencemaker/op/summarizer/comparative_extraction_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/comparative_extraction_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/comparative_extraction_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/comparative_extraction_prompt.yaml
diff --git a/experiencemaker/op/summarizer/experience_deduplication_op.py b/experiencemaker/experiencemaker/op/summarizer/experience_deduplication_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/experience_deduplication_op.py
rename to experiencemaker/experiencemaker/op/summarizer/experience_deduplication_op.py
diff --git a/experiencemaker/op/summarizer/experience_validation_op.py b/experiencemaker/experiencemaker/op/summarizer/experience_validation_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/experience_validation_op.py
rename to experiencemaker/experiencemaker/op/summarizer/experience_validation_op.py
diff --git a/experiencemaker/op/summarizer/experience_validation_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/experience_validation_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/experience_validation_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/experience_validation_prompt.yaml
diff --git a/experiencemaker/op/summarizer/failure_extraction_op.py b/experiencemaker/experiencemaker/op/summarizer/failure_extraction_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/failure_extraction_op.py
rename to experiencemaker/experiencemaker/op/summarizer/failure_extraction_op.py
diff --git a/experiencemaker/op/summarizer/failure_extraction_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/failure_extraction_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/failure_extraction_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/failure_extraction_prompt.yaml
diff --git a/experiencemaker/op/summarizer/pdf_preprocess_op.py b/experiencemaker/experiencemaker/op/summarizer/pdf_preprocess_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/pdf_preprocess_op.py
rename to experiencemaker/experiencemaker/op/summarizer/pdf_preprocess_op.py
diff --git a/experiencemaker/op/summarizer/simple_comparative_summary_op.py b/experiencemaker/experiencemaker/op/summarizer/simple_comparative_summary_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/simple_comparative_summary_op.py
rename to experiencemaker/experiencemaker/op/summarizer/simple_comparative_summary_op.py
diff --git a/experiencemaker/op/summarizer/simple_comparative_summary_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/simple_comparative_summary_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/simple_comparative_summary_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/simple_comparative_summary_prompt.yaml
diff --git a/experiencemaker/op/summarizer/simple_summary_op.py b/experiencemaker/experiencemaker/op/summarizer/simple_summary_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/simple_summary_op.py
rename to experiencemaker/experiencemaker/op/summarizer/simple_summary_op.py
diff --git a/experiencemaker/op/summarizer/simple_summary_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/simple_summary_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/simple_summary_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/simple_summary_prompt.yaml
diff --git a/experiencemaker/op/summarizer/success_extraction_op.py b/experiencemaker/experiencemaker/op/summarizer/success_extraction_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/success_extraction_op.py
rename to experiencemaker/experiencemaker/op/summarizer/success_extraction_op.py
diff --git a/experiencemaker/op/summarizer/success_extraction_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/success_extraction_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/success_extraction_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/success_extraction_prompt.yaml
diff --git a/experiencemaker/op/summarizer/trajectory_preprocess_op.py b/experiencemaker/experiencemaker/op/summarizer/trajectory_preprocess_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/trajectory_preprocess_op.py
rename to experiencemaker/experiencemaker/op/summarizer/trajectory_preprocess_op.py
diff --git a/experiencemaker/op/summarizer/trajectory_segmentation_op.py b/experiencemaker/experiencemaker/op/summarizer/trajectory_segmentation_op.py
similarity index 100%
rename from experiencemaker/op/summarizer/trajectory_segmentation_op.py
rename to experiencemaker/experiencemaker/op/summarizer/trajectory_segmentation_op.py
diff --git a/experiencemaker/op/summarizer/trajectory_segmentation_prompt.yaml b/experiencemaker/experiencemaker/op/summarizer/trajectory_segmentation_prompt.yaml
similarity index 100%
rename from experiencemaker/op/summarizer/trajectory_segmentation_prompt.yaml
rename to experiencemaker/experiencemaker/op/summarizer/trajectory_segmentation_prompt.yaml
diff --git a/experiencemaker/op/vector_store/__init__.py b/experiencemaker/experiencemaker/op/vector_store/__init__.py
similarity index 100%
rename from experiencemaker/op/vector_store/__init__.py
rename to experiencemaker/experiencemaker/op/vector_store/__init__.py
diff --git a/experiencemaker/op/vector_store/recall_vector_store_op.py b/experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py
similarity index 100%
rename from experiencemaker/op/vector_store/recall_vector_store_op.py
rename to experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py
diff --git a/experiencemaker/op/vector_store/update_vector_store_op.py b/experiencemaker/experiencemaker/op/vector_store/update_vector_store_op.py
similarity index 100%
rename from experiencemaker/op/vector_store/update_vector_store_op.py
rename to experiencemaker/experiencemaker/op/vector_store/update_vector_store_op.py
diff --git a/experiencemaker/op/vector_store/vector_store_action_op.py b/experiencemaker/experiencemaker/op/vector_store/vector_store_action_op.py
similarity index 100%
rename from experiencemaker/op/vector_store/vector_store_action_op.py
rename to experiencemaker/experiencemaker/op/vector_store/vector_store_action_op.py
diff --git a/experiencemaker/pipeline/__init__.py b/experiencemaker/experiencemaker/pipeline/__init__.py
similarity index 100%
rename from experiencemaker/pipeline/__init__.py
rename to experiencemaker/experiencemaker/pipeline/__init__.py
diff --git a/experiencemaker/pipeline/pipeline.py b/experiencemaker/experiencemaker/pipeline/pipeline.py
similarity index 100%
rename from experiencemaker/pipeline/pipeline.py
rename to experiencemaker/experiencemaker/pipeline/pipeline.py
diff --git a/experiencemaker/pipeline/pipeline_context.py b/experiencemaker/experiencemaker/pipeline/pipeline_context.py
similarity index 100%
rename from experiencemaker/pipeline/pipeline_context.py
rename to experiencemaker/experiencemaker/pipeline/pipeline_context.py
diff --git a/experiencemaker/schema/__init__.py b/experiencemaker/experiencemaker/schema/__init__.py
similarity index 100%
rename from experiencemaker/schema/__init__.py
rename to experiencemaker/experiencemaker/schema/__init__.py
diff --git a/experiencemaker/schema/app_config.py b/experiencemaker/experiencemaker/schema/app_config.py
similarity index 100%
rename from experiencemaker/schema/app_config.py
rename to experiencemaker/experiencemaker/schema/app_config.py
diff --git a/experiencemaker/schema/experience.py b/experiencemaker/experiencemaker/schema/experience.py
similarity index 100%
rename from experiencemaker/schema/experience.py
rename to experiencemaker/experiencemaker/schema/experience.py
diff --git a/experiencemaker/schema/message.py b/experiencemaker/experiencemaker/schema/message.py
similarity index 100%
rename from experiencemaker/schema/message.py
rename to experiencemaker/experiencemaker/schema/message.py
diff --git a/experiencemaker/schema/request.py b/experiencemaker/experiencemaker/schema/request.py
similarity index 100%
rename from experiencemaker/schema/request.py
rename to experiencemaker/experiencemaker/schema/request.py
diff --git a/experiencemaker/schema/response.py b/experiencemaker/experiencemaker/schema/response.py
similarity index 100%
rename from experiencemaker/schema/response.py
rename to experiencemaker/experiencemaker/schema/response.py
diff --git a/experiencemaker/schema/vector_node.py b/experiencemaker/experiencemaker/schema/vector_node.py
similarity index 100%
rename from experiencemaker/schema/vector_node.py
rename to experiencemaker/experiencemaker/schema/vector_node.py
diff --git a/experiencemaker/service/__init__.py b/experiencemaker/experiencemaker/service/__init__.py
similarity index 100%
rename from experiencemaker/service/__init__.py
rename to experiencemaker/experiencemaker/service/__init__.py
diff --git a/experiencemaker/service/experience_maker_client.py b/experiencemaker/experiencemaker/service/experience_maker_client.py
similarity index 100%
rename from experiencemaker/service/experience_maker_client.py
rename to experiencemaker/experiencemaker/service/experience_maker_client.py
diff --git a/experiencemaker/service/experience_maker_service.py b/experiencemaker/experiencemaker/service/experience_maker_service.py
similarity index 100%
rename from experiencemaker/service/experience_maker_service.py
rename to experiencemaker/experiencemaker/service/experience_maker_service.py
diff --git a/experiencemaker/service/mcp_client.py b/experiencemaker/experiencemaker/service/mcp_client.py
similarity index 100%
rename from experiencemaker/service/mcp_client.py
rename to experiencemaker/experiencemaker/service/mcp_client.py
diff --git a/experiencemaker/tool/__init__.py b/experiencemaker/experiencemaker/tool/__init__.py
similarity index 100%
rename from experiencemaker/tool/__init__.py
rename to experiencemaker/experiencemaker/tool/__init__.py
diff --git a/experiencemaker/tool/base_tool.py b/experiencemaker/experiencemaker/tool/base_tool.py
similarity index 100%
rename from experiencemaker/tool/base_tool.py
rename to experiencemaker/experiencemaker/tool/base_tool.py
diff --git a/experiencemaker/tool/code_tool.py b/experiencemaker/experiencemaker/tool/code_tool.py
similarity index 100%
rename from experiencemaker/tool/code_tool.py
rename to experiencemaker/experiencemaker/tool/code_tool.py
diff --git a/experiencemaker/tool/dashscope_search_tool.py b/experiencemaker/experiencemaker/tool/dashscope_search_tool.py
similarity index 100%
rename from experiencemaker/tool/dashscope_search_tool.py
rename to experiencemaker/experiencemaker/tool/dashscope_search_tool.py
diff --git a/experiencemaker/tool/mcp_tool.py b/experiencemaker/experiencemaker/tool/mcp_tool.py
similarity index 100%
rename from experiencemaker/tool/mcp_tool.py
rename to experiencemaker/experiencemaker/tool/mcp_tool.py
diff --git a/experiencemaker/tool/tavily_search_tool.py b/experiencemaker/experiencemaker/tool/tavily_search_tool.py
similarity index 97%
rename from experiencemaker/tool/tavily_search_tool.py
rename to experiencemaker/experiencemaker/tool/tavily_search_tool.py
index 0236c583..9d1c564d 100644
--- a/experiencemaker/tool/tavily_search_tool.py
+++ b/experiencemaker/experiencemaker/tool/tavily_search_tool.py
@@ -29,7 +29,7 @@ class TavilySearchTool(BaseTool):
enable_print: bool = Field(default=True)
enable_cache: bool = Field(default=False)
cache_path: str = Field(default="./web_search_cache")
- topic: Literal["general", "news", "finance"] = Field(default="general")
+ topic: Literal["general", "news", "finance"] = Field(default="general", description="finance, general")
_client: TavilyClient | None = PrivateAttr()
@@ -106,4 +106,4 @@ if __name__ == "__main__":
load_dotenv()
tool = TavilySearchTool()
- tool.execute(query="恒生医药为什么一直涨")
+ tool.execute(query="A股医药为什么一直涨")
diff --git a/experiencemaker/tool/terminate_tool.py b/experiencemaker/experiencemaker/tool/terminate_tool.py
similarity index 100%
rename from experiencemaker/tool/terminate_tool.py
rename to experiencemaker/experiencemaker/tool/terminate_tool.py
diff --git a/experiencemaker/utils/__init__.py b/experiencemaker/experiencemaker/utils/__init__.py
similarity index 100%
rename from experiencemaker/utils/__init__.py
rename to experiencemaker/experiencemaker/utils/__init__.py
diff --git a/experiencemaker/utils/common_utils.py b/experiencemaker/experiencemaker/utils/common_utils.py
similarity index 100%
rename from experiencemaker/utils/common_utils.py
rename to experiencemaker/experiencemaker/utils/common_utils.py
diff --git a/experiencemaker/utils/file_handler.py b/experiencemaker/experiencemaker/utils/file_handler.py
similarity index 100%
rename from experiencemaker/utils/file_handler.py
rename to experiencemaker/experiencemaker/utils/file_handler.py
diff --git a/experiencemaker/utils/http_client.py b/experiencemaker/experiencemaker/utils/http_client.py
similarity index 100%
rename from experiencemaker/utils/http_client.py
rename to experiencemaker/experiencemaker/utils/http_client.py
diff --git a/experiencemaker/utils/op_utils.py b/experiencemaker/experiencemaker/utils/op_utils.py
similarity index 100%
rename from experiencemaker/utils/op_utils.py
rename to experiencemaker/experiencemaker/utils/op_utils.py
diff --git a/experiencemaker/utils/registry.py b/experiencemaker/experiencemaker/utils/registry.py
similarity index 100%
rename from experiencemaker/utils/registry.py
rename to experiencemaker/experiencemaker/utils/registry.py
diff --git a/experiencemaker/utils/singleton.py b/experiencemaker/experiencemaker/utils/singleton.py
similarity index 100%
rename from experiencemaker/utils/singleton.py
rename to experiencemaker/experiencemaker/utils/singleton.py
diff --git a/experiencemaker/utils/timer.py b/experiencemaker/experiencemaker/utils/timer.py
similarity index 100%
rename from experiencemaker/utils/timer.py
rename to experiencemaker/experiencemaker/utils/timer.py
diff --git a/experiencemaker/vector_store/__init__.py b/experiencemaker/experiencemaker/vector_store/__init__.py
similarity index 100%
rename from experiencemaker/vector_store/__init__.py
rename to experiencemaker/experiencemaker/vector_store/__init__.py
diff --git a/experiencemaker/vector_store/base_vector_store.py b/experiencemaker/experiencemaker/vector_store/base_vector_store.py
similarity index 100%
rename from experiencemaker/vector_store/base_vector_store.py
rename to experiencemaker/experiencemaker/vector_store/base_vector_store.py
diff --git a/experiencemaker/vector_store/chroma_vector_store.py b/experiencemaker/experiencemaker/vector_store/chroma_vector_store.py
similarity index 100%
rename from experiencemaker/vector_store/chroma_vector_store.py
rename to experiencemaker/experiencemaker/vector_store/chroma_vector_store.py
diff --git a/experiencemaker/vector_store/es_vector_store.py b/experiencemaker/experiencemaker/vector_store/es_vector_store.py
similarity index 100%
rename from experiencemaker/vector_store/es_vector_store.py
rename to experiencemaker/experiencemaker/vector_store/es_vector_store.py
diff --git a/experiencemaker/vector_store/file_vector_store.py b/experiencemaker/experiencemaker/vector_store/file_vector_store.py
similarity index 100%
rename from experiencemaker/vector_store/file_vector_store.py
rename to experiencemaker/experiencemaker/vector_store/file_vector_store.py
diff --git a/library/appworld_v1.jsonl b/experiencemaker/library/appworld_v1.jsonl
similarity index 100%
rename from library/appworld_v1.jsonl
rename to experiencemaker/library/appworld_v1.jsonl
diff --git a/library/bfcl_v1.jsonl b/experiencemaker/library/bfcl_v1.jsonl
similarity index 100%
rename from library/bfcl_v1.jsonl
rename to experiencemaker/library/bfcl_v1.jsonl
diff --git a/library/index.html b/experiencemaker/library/index.html
similarity index 100%
rename from library/index.html
rename to experiencemaker/library/index.html
diff --git a/pyproject.toml b/experiencemaker/pyproject.toml
similarity index 100%
rename from pyproject.toml
rename to experiencemaker/pyproject.toml
diff --git a/test/config.json b/experiencemaker/test/config.json
similarity index 100%
rename from test/config.json
rename to experiencemaker/test/config.json
diff --git a/test/test1.py b/experiencemaker/test/test1.py
similarity index 100%
rename from test/test1.py
rename to experiencemaker/test/test1.py
diff --git a/test/test2.py b/experiencemaker/test/test2.py
similarity index 100%
rename from test/test2.py
rename to experiencemaker/test/test2.py
diff --git a/test/test3.py b/experiencemaker/test/test3.py
similarity index 100%
rename from test/test3.py
rename to experiencemaker/test/test3.py
diff --git a/test/test4.py b/experiencemaker/test/test4.py
similarity index 100%
rename from test/test4.py
rename to experiencemaker/test/test4.py
diff --git a/experiencemaker/tool/web_search_cache/default.jsonl b/experiencemaker/tool/web_search_cache/default.jsonl
deleted file mode 100644
index d6db8afd..00000000
--- a/experiencemaker/tool/web_search_cache/default.jsonl
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "恒生医药为什么一直涨": {
- "query": "恒生医药为什么一直涨",
- "follow_up_questions": null,
- "answer": "恒生医药持续涨价,因政策支持和市场乐观情绪推动。创新药市场增长和资本流入增强了信心。ETF持续表现优异。",
- "images": [],
- "results": [
- {
- "url": "https://finance.sina.com.cn/stock/marketresearch/2024-05-09/doc-inaurxyc6090539.shtml",
- "title": "为什么说恒生医药进入中期布局拐点? - 新浪财经",
- "content": "受益于“五一”期间中国经济复苏验证的乐观判断,全球资本加速流入香港市场,港股科技板块全线拉升。恒生科技指数(HSTECH.HI)再度拉涨4.45%,恒生香港上市生物",
- "score": 0.69573146,
- "raw_content": "\n\n\n\n# 为ä»ä¹è¯´æçå»è¯è¿å
¥ä¸æå¸å±æç¹ï¼\n\nããçè¡å°±ç[ééºéºåæå¸ç æ¥](https://finance.sina.com.cn/stock/reportch/jinqilin.shtml)ï¼æå¨ï¼ä¸ä¸ï¼åæ¶ï¼å
¨é¢ï¼å©æ¨æææ½å䏻颿ºä¼ï¼\n\nããæ¥æºï¼å¸ä¸è¡å®¶\n\nãã为ä»ä¹è¯´æçå»è¯è¿å
¥ä¸æå¸å±æç¹ï¼\n\nããâäºä¸âååï¼ä¸å½èµäº§æäºå
¨çèµéççé¨âæå¡âæ¿åï¼æ¸¯è¡ã䏿¦è¡é¢è·å
¨çã\n\nãã转æç¹å¨4æ19æ¥å¨äºæé´ï¼ä¸å½è¯çä¼åå¸5é¡¹èµæ¬å¸åºå¯¹æ¸¯å使ªæ½ï¼æ¨å¨æå±ä¼åæ²ªæ·±æ¸¯éæºå¶ï¼å©å馿¸¯å·©åºæåå½é
éèä¸å¿å°ä½ã次å¨ä¸4æ22æ¥èµ·ï¼æ¸¯è¡ä¸»è¦ææ°ãç¾è¡ä¸æ¦è¡è¿ç»é¼ç©ºå¤§æ¶¨ã4æ23æ¥ï¼çé¶å°ä¸å½å¤§éåä¸å½é¦æ¸¯è¡ç¥¨è¯çº§ä¸è°è³è¶
é
ï¼ä¸ºè¿å¹´æ¥å¤èµå¤§è¡ç½è§å±å¤ä¸å½èµäº§ã\n\nãã5æ2æ¥ï¼ä¸èµè¡è¡æ
å¼çã\n\nããåçäºâäºä¸âæé´ä¸å½ç»æµå¤èéªè¯çä¹è§å¤æï¼å
¨çèµæ¬å éæµå
¥é¦æ¸¯å¸åºï¼æ¸¯è¡ç§ææ¿åå
¨çº¿æåãæçç§æææ°ï¼HSTECH.HIï¼ååº¦ææ¶¨4.45%ï¼æç馿¸¯ä¸å¸çç©ç§æææ°ï¼HSHKBIO.HIï¼æ´æ¯æ´æ¶¨4.7%ï¼ç¾è¡ä¸æ¦è¡ææ°ï¼US36966ï¼ä¹å¤§æ¶¨6.96%ãä»4æ22æ¥å°5æ6æ¥ï¼æçææ°ç½è§è¿ç»10ä¸ªäº¤ææ¥æ¶æ¶¨ï¼ç´¯è®¡ä¸æ¶¨14.51%ï¼é¢æâææ¯æ§çå¸âä¹å¿ã\n\n\n\nãã馿¸¯å¸åºæ ¸å¿ææ°è¡¨ç°ï¼æ°æ®æ¥æºï¼Windï¼\n\nããæç馿¸¯ä¸å¸çç©ç§æææ°æ¯æ¸¯è¡çç©å¶è¯ãåºå æµåºãç»èæ²»ççé¢åï¼é«ç§æã髿é¿ãé«å¼¹æ§çä»£è¡¨ææ°ï¼æä»½è¡å
æ¬å¨é¦æ¸¯äº¤ææéè¿ä¸å¸è§å第18Aç« ä¸å¸çå
¬å¸ï¼åæ°è¯ãCXOçåæ²¿ç§æå«é尤为çªåºãæ¤é´å
¶å¨æ¸¯è¡æ ¸å¿ææ°ä¸ä¸æç¬ç§ç表ç°ï¼è¡¨æå
¨çèµéå¨é£é©å好æåæ¶å¯¹æ¸¯è¡å»è¯çç©æ¿åçç¹å«åéã\n\nããè¿æ¯ä¸ä¸ªå¼å¾é«åº¦å
³æ³¨çæ°å¨åï¼æ¸¯è¡å»è¯çç©æ¿åæè¿æ¥ä¸æå¸å±çæç¹çªå£ã\n\n\n\nããä¸å¹´éè¡è°æ´æè§åº\n\nããå¤ççï¼æç馿¸¯ä¸å¸çç©ç§æææ°äº2019å¹´12æ16æ¥åå¸ã2021å¹´6æ29æ¥æ¸é¡¶å岿é«ç¹2925.74ç¹ï¼å°2024å¹´4æ19æ¥è§¦åºæä½ç¹712.69ç¹ãæé´åç»è¿ä¸å¹´å·¨å¹
éè¡è°æ´ï¼å岿ç¹çæå¤§è°æ´å¹
度é«è¾¾75.64%ã\n\n\n\nããï¼æ°æ®æ¥æºï¼Windï¼æ¶é´ï¼2019.12.16~2024.5.8ï¼\n\nããæ¸¯è¡çç©ç§æææ°å叿¶æ£å¼æ°å ç«æ
çåï¼çç©å»è¯æ¿åå¨éæ±æ¨å¨ä¸æç»æé«ä¼°å¼ï¼ææ°èªå叿¥èµ·ï¼ççä¸å¹´åæ¶é´å
ï¼å°2021å¹´6æ28æ¥æ¶çä»·å岿髿¥æ¶ï¼è¶³è¶³ä¸æ¶¨äº96.54%ãåºæ¬é¢ä»·å¼å¨è¾çæ¶é´å
被æåéæ¯æ¯ææ°è§é¡¶çéè¦åå ã\n\nããè§é¡¶æ·±è·çå¤é¨æ¨å¨å ç´ ï¼åæ¯ç¾å½å¯¹ä¸ç¾ç§æäº¤æµçæ¿ç转åã2021å¹´5æ12æ¥ï¼ç¾å½å½ä¼åè®®é¢åå¡å§åä¼éè¿âæ 尽忲¿æ³æ¡âï¼æ¹åå¨5å¹´å
æ¨æ¬¾1100å¤äº¿ç¾å
ç¨äºæèµäººå·¥æºè½ãå导ä½ãéå计ç®ãå
è¿éä¿¡ãçç©ææ¯åå
è¿è½æºçå
³é®ææ¯é¢åçåºç¡åé«çº§ç ç©¶ãåä¸å以åæè²åå¹è®è®¡åï¼ä»¥åºå¯¹æ¥èªä¸å½æ¥ç严éçç«äºååãå¾å¿«ï¼6æ8æ¥ï¼ç¾åè®®é¢éè¿å级ççã2021ç¾å½åæ°åç«äºæ³æ¡ãï¼æèµé颿åå°2500亿ç¾å
ã\n\nããèªé«ç¹åä¸ï¼çç©ç§æææ°æç»åè¾¹ä¸æ«è¿ä¸å¹´æ¶é´æå¼å¯éè¡æ´çã2024å¹´1æï¼ç¾å½åæãçç©å®å
¨æ³æ¡ãï¼ä¸å½åæ°è¯ãCXOä¼ä¸åéå²å»ï¼çç©ç§æææ°å¼å§éæ¥è§¦åºã\n\nããWindæ°æ®æ¾ç¤ºï¼æªè³2024å¹´5æ8æ¥ï¼æç馿¸¯ä¸å¸çç©ç§æææ°å¨è¿ç»åå¼¹ä¹åï¼å¸ççTTMä¹ä»
为28.64åï¼å¤äºåå²åä½ç¹2.59%çæä½ä½ç½®ï¼è¿ä½äºåå²ä¸ä½å¼ç90.9åãæå¤§å¼ç2126.3åï¼å¸åç1.72åï¼å¤äºåå²åä½ç¹3.93%ï¼å¸éç0.85åï¼å¤äºåå²åä½ç¹3.49%ï¼ä¿®å¤ç©ºé´å·¨å¤§ï¼å½å估弿§ä»·æ¯ãå®å
¨è¾¹é
åè¾é«ã\n\n\n\nããåæ°è¯è·å
¨æ¹ä½æ¯æ\n\nããå¨å¸åºæ
ç»ªè½¬ææ¶ï¼å»è¯çç©æ¹ååå°èµéå好ç大é»è¾æ¯ï¼èé¾åå éèæ¯ä¸ï¼å»ç弿¯å¨éæ±ç«¯æ¯è¾ä¸ºåæ§çã4æä¸æ¬ï¼é对沪深港å¸åºçç©ç§æãå»è¯æ¿åè¡æ
ï¼ä¸æµ·è¯å¸åæï¼éæ±ç«¯åä¾ç»ç«¯6大å ç´ å°å
±åæ¨å¨æ¶è´¹å»çå¸åºå¢é¿ã\n\nããéæ±ç«¯ï¼ï¼1ï¼æ¿ç红å©éæ¾ãâå¥åº·ä¸å½2030âå¥å
¨å
¨æ°å¥åº·å¶åº¦ä½ç³»ï¼æ¨è¿ä»¥âæ²»ç
为ä¸å¿âåâ以å¥åº·ä¸ºä¸å¿â转åãï¼2ï¼æ¯ä»è½åæåã对æ åè¾¾å½å®¶ç»éªï¼ä¸å½å¥åº·äº§ä¸æ¯ä»è½å仿è¾å¤§æå空é´ãï¼3ï¼äººå£ç»æååãéçèé¾åå éï¼å
·ææ´å¼ºç»æµå®åä¸æè²æ°´å¹³çâ60åâæ¥å
¥é伿½®ï¼å»çå¥åº·è¿æ¥æºçéæ±ãï¼4ï¼åç«æ
æ¶ä»£æ´å å
³æ³¨å¥åº·ã大伿¥å¸¸ä¿å¥æè¯æåï¼è®©ä¿å
»çææä¸ºå¯æç»çæ¥å¸¸ã\n\nããä¾ç»ç«¯ï¼ï¼1ï¼äº§å转å忰驱å¨ï¼åææåä¸å éè¿ä»£ï¼æçå¤å
ååå±ï¼æ»¡è¶³æ¶è´¹è
夿 ·åéæ±ã2023å¹´ä¿å¥é£å夿¡3632款ï¼åæ¯å¢é¿52%ãï¼2ï¼æ°ååå»ççæå¸å±æ¥è»å®åï¼ææé¾æ¥å¥åº·æ¶è´¹ç¾¤ä½ï¼å
æ¬ä¿¡æ¯ç§æ®ãè¿ç¨é®è¯ã线ä¸è´è¯ãç¾ç
管çãå
¶ä¸ä¿¡æ¯ç§æ®éè¿å类社交平å°ãæç´¢å¼æçæä¾ä¿å¥å
»çãç¾ç
è¯æ²»çç¸å
³ä¿¡æ¯ï¼æ¸éç大äº60%ã\n\nããè¿æ¥ï¼å¨å¤é¨ååä¸ï¼åæ°è¯è¡ä¸éæ¥é
é
¿å
¨æ¹ä½æ¯ææ¿çãä»å¹´3æâåæ°è¯â馿¬¡åå
¥å½å¡é¢æ¿åºå·¥ä½æ¥åï¼å
·ä½æ¯ææ¿çæ£æ¸æ¬¡æ¨åºã4æ7æ¥ï¼å京å¸å»çä¿éå±ç9é¨é¨å¶å®ãåäº¬å¸æ¯æåæ°å»è¯é«è´¨éåå±è¥å¹²æªæ½ï¼2024ï¼ï¼å¾æ±æè§ç¨¿ï¼ãã4æ11æ¥ï¼å½å®¶å»ä¿å±åå¸ã2023å¹´å»çä¿éäºä¸åå±ç»è®¡å¿«æ¥ãï¼éè¿å»ºç«è¦çç³æ¥ãè¯å®¡ãæµç®ãè°å¤çå
¨æµç¨çåæ°è¯æ¯ææºå¶ï¼æ¯ææ´å¤æ°è¯å¥½è¯çº³å
¥å»ä¿ï¼2023ï¼å¹´ç®å½è°æ´ä¸ï¼æï¼57ï¼ä¸ªè¯åå®ç°äºâå½å¹´è·æ¹ãå½å¹´çº³å
¥ç®å½âã\n\nããåæ°è¯ä½ä¸ºæ°è´¨ç产å代表ï¼âæ¿çå
âæ¥ç»åºå°ï¼å»è¯äº§ä¸è¿æ¥æ·±å»åé©ï¼å¸åºå¯¹å»è¯æ¿åãåæ°è¯äº§ä¸çæ²è§é¢æä¹æ£å¨æ ¹æ¬æè½¬ã\n\nããäºå®ä¸ï¼2022å¹´~2023å¹´å
¨çè¯ä¼ç®¡çº¿è§æ¨¡æåå25å䏿3ï¼å®¶ä¸å½ä¼ä¸ä¸æ¦ï¼è¡¨ææå½å¨å
¨çè¯ç©åæ°ä¸å´é²å¤´è§ãâååäºâæé´ï¼æå½åæ°è¯æ£ç§¯æå®ç°é«è´¨éâå¼è¿æ¥âå髿°´å¹³âèµ°åºå»âã2021å¹´~2022ï¼å¹´ï¼ä¸å½åæ°è¯ææå¼è¿ï¼license-inï¼äº¤æé¡¹ç®å¤æ¶åCDååãRNAè¯ç©çèµéãåæ¶ï¼[ç¾æµç¥å·](https://finance.sina.com.cn/realstock/company/sh688235/nc.shtml)çæ³½å¸æ¿å°¼å®ç°äºæ¬åæ°è¯åºæµ·âé¶ççªç ´âï¼ä¸å¯¹å¤ææï¼license-outï¼ä¹æ·æ¥é¢é¢ãæ®å»è¯éæ¹æ°æ®ç»è®¡ï¼2022ï¼å¹´ï¼æå½license-outäº¤ææ»éé¢è¾¾277亿ç¾å
ï¼è¾2020å¹´å¢é¿122.08%ãï¼æ¥æºï¼ã[ä¸å½å»è¯](https://finance.sina.com.cn/realstock/company/sh600056/nc.shtml)产ä¸é«è´¨éåå±ç¶åµè°ç ï¼2021â2023ï¼ãç ç©¶æ¥å综述ï¼ä¸å½é£åè¯åç管æå¿2024å¹´ç¬¬ä¸æï¼\n\nãã[å½éè¯å¸](https://finance.sina.com.cn/realstock/company/sh600109/nc.shtml)认为ï¼å
¨çåæ°è¯æ¯ç§æè¿æ¥å
ç°çéè¦é¢åï¼ä¸å½åæ°è¯äº§ä¸é¾æ£å¨å´èµ·ï¼é¢å
è¯çå
¨é¢æ¢å¤ï¼å½å®¶å¯¹åæ°è¯çé¼å±æ¯ææ¿çéç»åºå°ï¼ä»¥åä¸å½è¯ä¼å½é
忥ä¼çæ¨è¿ï¼è¡ä¸æ´ä½å°è¿æ¥ä¸ç»©ä¸å¸åºè¡¨ç°ç忥åä¸ã[ä¸é¶è¯å¸](https://finance.sina.com.cn/realstock/company/sh601696/nc.shtml)表示ï¼å»è¯çç©è¡ä¸ä¼´éâå»è¯åè
â影忶éååºæ°ååæ¶åï¼åç»ä¸ç»©å¢éææé壿¹åï¼å½å
æ¿çæ¯æååæ°è¯åºæµ·åæ¯åå¼å¾æå¾
ã\n\n\n\nããç¾éæ¯å¨æå©å¥½ç§æ\n\nããä»å¤é¨ç¯å¢èè¨ï¼è¿ææ¸¯è¡æç»ä¸æ¶¨ï¼æåç¾å
鿝卿æ¥ä¸´ãæ¥æ¬ç»§ç»å æ¯æ¦çéä½çå½±åï¼å¤èµé¨åæµå港è¡å¸åºã\n\nãã卿¸¯è¡5ææèµçç¥ä¸ï¼[å½ä¿¡è¯å¸](https://finance.sina.com.cn/realstock/company/sz002736/nc.shtml)è®¤ä¸ºï¼æ¸¯è¡ææåè²é¿æåºæ¬é¢ä¿®å¤è¡æ
ãéçæçç§æå¤å¹´æ¥ä¼°å¼åºé¨çç¡®è®¤ï¼æçææ°ãæç港è¡éãæçé«è¡æ¯ãæçç§æçä¼°å¼åºé¨å
¨é¨ç¡®è®¤å®æ¯ï¼å°ææ¶ä¸ç´ä»¥æ¥âæä¼°å¼âçæå¤§ä¸ç¡®å®æ§ï¼æå©äºé¿çº¿èµéåæµè¿äºå
¬å¸å¹¶å¼å¯é¿æåºæ¬é¢ä¿®å¤ã\n\nãã[彿³°åå®](https://finance.sina.com.cn/realstock/company/sh601211/nc.shtml)ç æ¥è¡¨ç¤ºï¼æ¸¯è¡å¨è¿æå¤§å¹
èµ°é«åï¼ä¼åºç°ä¸å®åæ§åéè¡ï¼ä½éçä¸å½å¨ç»æµãæ¿çé¢åä¸ç¡®å®æ§éä½ï¼æ¸¯è¡ä»æå¯ä¸ºç©ºé´ãå½å
æ¹é¢ï¼ç»æµæ¿ç以稳为主ï¼å°äº§æ¿çæç»ä¼åï¼æ¸¯è¡å¸åºçå©é¢ææ¹åï¼å¤èµä¸ä¿®å½å
ç»æµåæ¯ï¼æ¸¯è¡å¸åºæ
绪修å¤ãæµ·å¤æ¹é¢ï¼ç¾èå¨åµæ¤éèåå³å¨åå¸åºçæå¾ææ¾ï¼å¸åºæµå¨æ§è¿ä¸æ¥æ¶ç´§çé£é©ä¸éã\n\nããèå»è¯çç©ãåæ°è¯ãCXOäº§ä¸æ¹åï¼é¤äºå
³æ³¨åºæ¬é¢è¶å¿ï¼ä½ä¸ºç§æä¸»çº¿ï¼ä¼°å¼è¿åå°å
¨çèµéæµå¨ï¼å°¤å
¶ç¾å
å¨æçæ·±å»å½±åãè¿æï¼è½ç¶ç¾èå¨éæ¯é¢æææå»¶åï¼ä½ç¾å
鿝卿å¼å¯çå¤æå¹¶æªæ¹åï¼å¯¹ç§æåæ°ä¸»çº¿çä¼°å¼å½¢ææå©çå
¨çæµå¨æ§è¾¹é
æ¹åç¯å¢ã\n\nããåæ°ä¸»çº¿æ¯ç©¿è¶å¨æçæ³å®ãçç©ç§ææ¯å»è¯è¡ä¸åæ°åº¦é«ãæé¿ç©ºé´å¤§ç主æµèµéãæç馿¸¯ä¸å¸çç©ç§æææ°å·²åºæ¬å®ææ¿çåºãåºæ¬é¢åºãä¼°å¼åºãèµéé¢åºçå
±æ¯ï¼éåè¿è¡ä¸é¿å¨æå¸å±ææï¼å享å»è¯çç©é«æé¿æ ¸å¿èµäº§çå¤å©å¢é¿ã\n\nããè¿æä»½é¢æç»å¢é¿ãå弿æ¾ä¿®å¤ãåºå
ä»·æ ¼æç»åå¼¹çæçå»è¯ETFï¼159892.SZï¼ï¼æ£æ¯è·è¸ªæç馿¸¯ä¸å¸çç©ç§æææ°çé«å
åç§ï¼å¨è·è¸ªåææ°çETF产åä¸ï¼æçå»è¯ETFçåºéè§æ¨¡ãæäº¤é¢æå¤§ï¼æµå¨æ§è¯å¥½ä¸æ¯æT+0交æãåæ¶ï¼æçå»è¯ETF乿坹åºçåºå¤èæ¥åºéï¼Aç±»ï¼016970ï¼Cç±»ï¼016971ï¼ï¼è´¹çå¨åç±»ä¸å±æä½æ¡£æ°´å¹³ï¼æ²¡æè¡ç¥¨è´¦æ·çåºå¤æèµè
ä¹å¯ä»¥å
³æ³¨ã\n\nããå¼å¾ä¸æçæ¯ï¼æ¸¯è¡å»è¯çç©æ¿åå ä¸å¸å¶åº¦å·®å¼ï¼å
å«äºä¸äºæªè½å¨Aè¡ä¸å¸çä¼è´¨ç¨ç¼ºççç©å»è¯ä¼ä¸ï¼æç馿¸¯ä¸å¸çç©ç§æææ°åå大æåè¡ä¸ç»å¤§é¨å齿²¡æå¨Aè¡ä¸å¸ãè¿ä¹æ¯æ¸¯è¡å»è¯çç©æ¿ååºå«Aè¡çæ ¸å¿èµäº§ä¼å¿ï¼è¯¥ææ°åå大æåè¡æ¶µçå个ç»åé¢åçé¾å¤´ä¼ä¸ï¼è¦ççç©å»è¯äº§ä¸é¾ä¸ä¸ä¸æ¸¸å个ç»åç¯èã\n\nããæ»ä¹ï¼éè¿æçå»è¯ETF䏿½åä¹°å
¥äºå¨æ¸¯ä¸å¸ç䏿µæ°èå®åè¯ä¼ï¼å¨å¨ææç¹å¸å±æå¯æ¥æè¾å¼ºçç©¿è¶å¨æè½åã\n\n\n\n\n责任ç¼è¾ï¼æ¨çº¢å\n\n\n\n\n## VIPè¯¾ç¨æ¨è\n\n## [æ°æµªç´æ](http://live.finance.sina.com.cn/)\n\n![@@title@@]()\n\n### @@title@@\n\n## APPä¸äº«ç´æ\n\n## ç鍿¨è\n\n\n\n24å°æ¶æ»å¨ææ¥ææ°çè´¢ç»èµè®¯åè§é¢ï¼æ´å¤ç²ä¸ç¦å©æ«æäºç»´ç å
³æ³¨ï¼sinafinanceï¼\n\n## [è¡å¸ç´æ](http://live.finance.sina.com.cn)\n\n## [7X24å°æ¶](http://live.sina.com.cn/zt/f/v/finance/globalnews1)\n\n\n\n\n\n\n\n[æ°æµªè´¢ç»æè§åé¦çè¨æ¿](http://news.sina.com.cn/feedback/post.html)\n\n[æ°æµªç®ä»](http://ir.sina.com.cn/index.shtml)|[广åæå¡](http://emarketing.sina.com.cn/)|[About Sina](http://corp.sina.com.cn/eng/)\n \n[èç³»æä»¬](http://www.sina.com.cn/contactus.html)|[æèä¿¡æ¯](http://corp.sina.com.cn/chn/sina_job.html)|[éè¡è¯æ³¨å](https://login.sina.com.cn/signup/signup.php)\n \n[产åçç](http://help.sina.com.cn/)|[ç½ç«å¾å¸](http://www.sina.com.cn/intro/lawfirm.shtml)|[SINA English](http://english.sina.com)\n\nCopyright © 1996-2024 SINA Corporation\n\nAll Rights Reserved æ°æµªå
¬å¸ [çæææ](http://www.sina.com.cn/intro/copyright.shtml)"
- },
- {
- "url": "https://popcj.com/depth/0204832508379321",
- "title": "港股医药为何尾盘暴涨?原因找到了 - 古东管家",
- "content": "港股医药板块尾盘大幅拉升,恒生生物科技指数收盘涨3.86%,在全市场指数中领先,跟踪该指数的恒生医药ETF(159892)明日或有补涨可能。",
- "score": 0.6747586,
- "raw_content": "\n\n\n\n\n# 港股医药为何尾盘暴涨?原因找到了\n\n2025-08-05 17:42:24\n\n港股[医药板块](https://www.popcj.com/search/939)尾盘大幅拉升,恒生生物[科技](https://www.popcj.com/search/200)指数收盘涨3.86%,在全市场指数中领先,跟踪该指数的恒生[医药ETF](https://www.popcj.com/search/22757)(159892)明日或有补涨可能。\n\n具体到[个股](https://www.popcj.com/search/5625)来看,[君实生物](https://www.popcj.com/company/68818031)(01877.HK)尾盘飙升,一度近35%。君实生物此前已于6月20日完成配售4100万股新股事项,本次配售的募集资金净额约为10.26亿港元。[公司](https://www.popcj.com/search/1598)拟将配售募集资金净额的70%用于创[新药研发](https://www.popcj.com/search/2541)。\n\n乐普生物大涨超14%,MRG004A启动III期临床,MRG007完成首例患者入组,MRG007近期拿下4700万美元的授权交易(首付款+近期里程碑付款)。\n\n国金[证券](https://www.popcj.com/search/23)此前表示,全球双抗开发热情高涨,国产抗体行业迎来创新成果兑现窗口期。君实生物对单抗开发具有丰富经验,其开发的PD-1单抗分子特瑞普利是最早上市的国产PD-1抑制剂,目前特瑞普利在国内上市众多适应症,同时持续拓展全球商业化网络。\n\n*免责声明:所有平台仅提供服务对接功能,资讯信息、数据资料来源于第三方,其中发布的文章、视频、数据仅代表内容发布者个人的观点,并不代表泡财经平台的观点,不构成任何投资建议,仅供参考,用户需独立做出投资决策,自行承担因信赖或使用第三方信息而导致的任何损失。投资有风险,入市需谨慎。*\n\n\n\n请先登录后发表评论\n\n## 共*0*条评论\n\n扫码在手机上查看\n\n## 相关文章\n\n### 南天信息(000948):中标中国银行股份有限公司广东省分行采购项目,中标金额为167.72万元\n\n\n\n### 电气风电(688660):中标重庆广酉新能源有限公司采购项目,中标金额为2.17亿元\n\n\n\n### 创新药熄火,医疗板块活跃\n\n\n\n[联系我们](/about)**|** 媒体合作 popcj@gudongtech.com**|** 投诉建议 popcj@gudongtech.com\n\n[粤ICP备2020093236号-3](https://beian.miit.gov.cn/)**|** [粤ICP备2020093236号-4](https://beian.miit.gov.cn/)**|** [用户协议](/user/agreement)**|** [隐私政策](/user/privacy)**|** [网站地图](https://www.gudongtech.com/baidu/sitemap.txt)\n\n郑重声明:古东管家所有平台仅提供服务对接功能,其中发布的文章、视频、数据仅代表内容发布者个人的观点,与古东管家平台的立场无关,不对您构成任何投资建议,仅供参考。用户需独立做出投资决策,风险自担。投资有风险,选择需谨慎。\n\n版权所有 © 深圳市古东管家科技有限责任公司\n\n\n\n扫码下载古东管家APP"
- },
- {
- "url": "https://m.lejucaijing.com/news-7355823170131785420.html?&originflag=commonshare",
- "title": "创新药持续大涨,恒生医药ETF再创新高,多只个股翻倍 - 乐居财经",
- "content": "7月29日早盘,港股医药延续上涨行情,恒生医药ETF(159892)盘中涨超1%,2025年涨幅超86%,在全市场ETF中涨幅靠前;其持仓股三生制药年内涨幅超400%。",
- "score": 0.5948579,
- "raw_content": "## \n\n\n\n## 热门搜索\n\n## 搜索历史[清空](javascript:;)\n\n## 创新药持续大涨,恒生医药ETF再创新高,多只个股翻倍\n\n[有连云](https://m.lejucaijing.com/column-771.html) 2025-07-29 12:52 4122阅读\n\n7月29日早盘,港股医药延续上涨行情,恒生医药ETF(159892)盘中涨超1%,2025年涨幅超86%,在全市场ETF中涨幅靠前;其持仓股三生制药年内涨幅超400%,[诺诚健华](https://m.lejucaijing.com/company/74055.html)、乐普生物、[信达生物](https://m.lejucaijing.com/company/74035.html)等年内涨超100%。\n\n消息面来看,国家育儿补贴制度实施方案7月28日公布。从2025年1月1日起,对符合法律法规规定生育的3周岁以下婴幼儿发放补贴,至其年满3周岁。育儿补贴按年发放,现阶段国家基础标准为每孩每年3600元。\n\n7月28日,[恒瑞医药](https://m.lejucaijing.com/company/64873.html)公告与生物制药公司GSK达成协议,将HRS-9821项目的全球独家权利和至多11个项目的全球独家许可的独家选择权有偿许可给GSK。GSK将向恒瑞支付5亿美元的首付款,里程碑付款的潜在总金额约120亿美元。\n\n来源:有连云\n\n\n\n\n《乐居财经精选》\n\n### 民营房企反扑\n\n\n\n\n#### 相关文章\n\n#### 24小时热门文章\n\n#### 最新文章\n\n## 评论\n\n点击下载App参与更多互动\n\n### 前往乐居财经APP查看原文,体验更佳\n\n## 关注\n\n## 原创\n\n## 数据\n\n## 榜单\n\n"
- },
- {
- "url": "https://guba.eastmoney.com/news,sz159892,1544579467.html",
- "title": "$恒生医药ETF(SZ159892)$这个ETF一直是折价,什么原因? - 股吧",
- "content": "$恒生医药ETF(SZ159892)$ 这个ETF一直是折价,什么原因? 发布于东方财富 ... 24小时点击排行. 美股大涨A股也有利好. 信号来了,支撑反弹水到渠成! 中大力德",
- "score": 0.56489134,
- "raw_content": null
- },
- {
- "url": "https://www.stcn.com/article/detail/2347585.html",
- "title": "又涨了!创新药ETF还能疯多久? - 证券时报",
- "content": "今年以来,涨幅最大的赛道当属港股创新药,相关指数的最大涨幅已超60%。受访者向《国际金融报》记者表示,今日创新药的大涨主要得益于政策面的有力催化。当前",
- "score": 0.2578636,
- "raw_content": null
- }
- ],
- "response_time": 4.45
- }
-}
\ No newline at end of file
diff --git a/experiencemaker/tool/web_search_cache/test b/experiencemaker/tool/web_search_cache/test
deleted file mode 100644
index 0eb232f6..00000000
--- a/experiencemaker/tool/web_search_cache/test
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "results": [
- {
- "url": "https://www.stcn.com/article/detail/2347585.html",
- "raw_content": "又涨了!创新药ETF还能疯多久?\n===============\n\n首页\n快讯\n要闻\n股市\n新股信披+公司港美股数据基金金融视听评论专题产经创投科创板新三板投教ESG滚动 \n\n公众号 电子报客户端\n\n您当前的位置:证券时报>基金>正文\n又涨了!创新药ETF还能疯多久? \n来源:国际金融报 作者:夏悦超 2025-07-02 07:18\n点赞\n\n分享\n#### 微信扫一扫:分享 \n在多重利好因素的共同推动下,创新药相关ETF(交易型开放式指数基金)再次集体迎来大涨。\n7月1日,多只创新药主题ETF在午后出现放量大涨,收盘时涨幅超2%,在全市场ETF涨幅榜单中位居前列。其中,涨得最高的1只ETF涨幅超5%。\n从成交额来看,有1只港股创新药ETF的成交额达到了59.8亿元,排在全市场股票型ETF之前。\n今年以来,涨幅最大的赛道当属港股创新药,相关指数的最大涨幅已超60%。受访者向《国际金融报》记者表示,今日创新药的大涨主要得益于政策面的有力催化。当前,部分创新药企业的基本面呈现出良好的发展态势,有望在未来实现进一步的提升。然而,投资者也需警惕板块估值泡沫以及个股股价虚高的潜在风险。综合来看,预计板块整体的涨幅在后续可能会相对上半年更温和。\n放量大涨\n今日港股休市,跟踪港股市场的跨境ETF照常交易,尤以港股创新药相关ETF交投最火热。\nWind数据显示,截至7月1日收盘,多只创新药主题ETF领涨全市场ETF。具体来看,同时覆盖A股和港股医药的富国创新药ETF收涨5.88%,排在全市场第一;跟踪港股医药的汇添富港股通创新药ETF收涨4.44%,排在第二。此外,多只创新药ETF和港股通创新药ETF涨幅超2%,占据全市场ETF涨幅榜单的前列。\n\n数据来源:Wind\n从成交额来看,以全市场投资股票资产的ETF为例,广发港股创新药ETF当日成交额达到59.8亿元,排在全市场第一,汇添富港股通创新药ETF、银华港股创新药ETF当日成交额均突破10亿元。\n从ETF当日走势来看,多只创新药ETF的放量大涨主要集中在午后,且这种强劲的反弹表现一直持续到收盘前。从上述成交数据可以发现,由于当日港股休市,而大部分创新药企业又是在港股上市,因此部分看好创新药的资金通过A股大量买入港股通ETF,这其中包括多只港股创新药ETF。在港股休市时,港股通ETF场外申购和赎回业务被暂停,场内不断买入将会炒高溢价,类似的事情也曾发生在港股通在内的跨境ETF上。\n数据显示,截至7月1日,多只港股创新药ETF溢价率超3%,富国创新药ETF溢价率超5%。目前已有富国、汇添富等公募基金公司就相关ETF二级市场价格溢价的风险做出警示,并表示若溢价未有效回落,将采取临时停牌、延长停牌等措施警示风险。\n今年以来,创新药是涨势最强的赛道,相关指数集体走强。截至7月1日,恒生港股通创新药指数年内涨幅超60%,恒生生物科技指数年内涨幅超50%。\n未来行情\n近段时间,有关创新药的动态不断。\n6月30日,恒生港股通创新药指数作出了重大变更:指数将修订编制方案,明确剔除CXO等类型的公司,此类公司主要是医药外包服务类,意味着该指数将真正成为一只“纯度”100%的创新药指数。对此,易方达基金相关人士认为,这将更加精准反映我国创新药企的整体表现,能够更纯粹地聚焦以对外专利授权为主要商业模式的创新药公司,有效表征本轮创新药产业发展趋势。\n7月1日,国家医保局、国家卫生健康委印发《支持创新药高质量发展的若干措施》,其中提及,加大创新药研发支持力度,支持创新药进入基本医保药品目录和商业健康保险创新药品目录,支持创新药临床应用,提高创新药多元支付能力等。\n谈及今日创新药板块大涨的原因,止于至善投资总经理何理向《国际金融报》记者表示,主要受上述文件催化。\n“上述文件从研发端、支付端、销售入院端等多个角度给创新药发展有力的支持。从去年开始,国内就不断出台支持创新药发展的各项政策。预计在国内政策的支持下,创新药企业的研发方向会更有针对性,审批效率和抵达临床实际需求的效率也会提升。同时,医保和商保的联合支持有利于提升创新药的销售空间。”何理表示。\n今年上半年,在创新药板块大涨的背景下,部分主投该板块的基金取得了超85%的惊人涨幅。然而,随着涨幅的不断扩大,一些投资者也开始担忧:创新药行情能否持续,是否存在短期调整的风险。\n对此,融智投资基金经理兼高级研究员包金刚向记者分析称,在一系列鼓励政策的支持下,不少创新药企业实现了快速增长,盈利状况向好,这对估值提供了长期支撑。不过,部分资金可能会忽略创新药企业背后的风险,个别上市公司股价虚高,甚至出现了估值泡沫。\n“今年下半年,随着多款国产创新药有望实现出海授权、国家政策对于发展创新药的明确支持、数个国产重磅创新药物年内获批、创新药销售收入高速增长以及更多生物科技企业迎来商业化盈利,创新药板块的基本面有望进一步提升,板块情绪也有望保持积极。不过,整体板块涨幅可能较上半年更为温和。”包金刚表示。\n何理认为,从基本面来看,创新药是整个医药板块中成长性最强的板块。头部多家药企今年已经实现盈亏平衡或扭亏为盈,预计长期会有比较好的表现。从短期来看,不少管线预期已经充分反映在估值中,后续需要结合估值挑选有充足安全边际的个股。“当前头部几家创新药的估值反应较为充分,后续需要注意临床试验数据不及预期、海外政策限制等风险,目前可以布局一些安全边际还较高的创新药个股”。\n责任编辑: 冉超\n创新药ETF\n港股创新药\n港股通创新药ETF\n声明:证券时报力求信息真实、准确,文章提及内容仅供参考,不构成实质性投资建议,据此操作风险自担\n下载“证券时报”官方APP,或关注官方微信公众号,即可随时了解股市动态,洞察政策信息,把握财富机会。\n网友评论\n登录 后可以发言\n发送\n网友评论仅供其表达个人看法,并不表明证券时报立场\n暂无评论 \n为你推荐\n\n突然爆发!刚刚,狂掀涨停潮! 券商中国 周乐 2025-07-02 15:00 \n突然爆发!A股涨停潮、港股最高涨超130%,发生了什么? 证券时报 吴永芳 2025-07-02 17:29 \n菜鸟全球供应链再升级 亚太仓配网络覆盖10个国家地区 证券时报网 康殷 2025-07-02 14:06 \n达芙妮王俊刚:女鞋行业头部效应凸显 以模式转型与品牌焕新抢占增长先机 证券时报网 曹晨 2025-07-02 14:06 \n罕见!控制24个账户,持股超30%仍不报告!证监局:罚4250万 券商中国 刘艺文 2025-07-02 15:00 \n7月1日股票ETF资金净流出126亿元 中国基金报 天心 2025-07-02 15:14 \n\n时报 热榜\n换一换\n\n稳定币第一股申请银行牌照\n江苏战新基金集群亮相深圳!产业专项基金向头部GP抛出合作橄榄枝\n多家新能源车企,销量创新高!\n揭秘涨停丨迎政策利好,创新药多股涨停\n主力资金丨涨超280%,主力资金大幅出手这只新股!\n【公告精选】长春高新、欣旺达筹划发行H股\n扎克伯格宣布组建“超级智能实验室”,11人“天才团队”首曝光\n7月“金股”出炉!哪些公司最受青睐?赚钱机会在哪儿?\n\n热点 视频\n换一换\n\n国家发展改革委:以旧换新销售额已超1.4万亿元,第三批消费品以旧换新资金将在7月下达 06-26 13:06 \n充电宝召回风波持续发酵!民航局出台充电宝乘机新规:禁止旅客携带无3C标识及被召回的充电宝乘坐境内航班 06-27 14:41 \n耗资24亿的古城,四年累计亏损超10亿!张家界市委书记、市长表态:深刻汲取教训,努力盘活! 06-29 14:22 \n停职七个月后 苗华被免中央军委委员 06-29 14:23 \n深交所发布创业板“轻资产、高研发投入” 认定标准 06-30 17:01 \n\n关于我们|服务条例|联系我们|版权声明|网站地图|线索提交\n备案号:粤ICP备09109218号|增值电信业务经营许可证:粤B2-20080118|互联网新闻信息服务许可证10120170066\n违法和不良信息举报电话:0755-83514034 邮箱:bwb@stcn.com中央网信办违法和不良信息举报中心|证券时报网举报中心\n本网站提供之资料或信息,仅供投资者参考,不构成投资建议。\n深圳证券时报社有限公司版权所有,未经书面授权禁止转载及各种形式的软件开发。\nCopyright © 2008-2025 Shenzhen Securities Times Co., Ltd. All Rights Reserved\n站长统计站长统计",
- "images": [
-
- ]
- },
- {
- "url": "https://finance.sina.com.cn/stock/marketresearch/2024-05-09/doc-inaurxyc6090539.shtml",
- "raw_content": "为ä»ä¹è¯´æçå»è¯è¿å
¥ä¸æå¸å±æç¹ï¼\nããçè¡å°±çééºéºåæå¸ç æ¥ï¼æå¨ï¼ä¸ä¸ï¼åæ¶ï¼å
¨é¢ï¼å©æ¨æææ½å䏻颿ºä¼ï¼\nããæ¥æºï¼å¸ä¸è¡å®¶\nãã为ä»ä¹è¯´æçå»è¯è¿å
¥ä¸æå¸å±æç¹ï¼\nããâäºä¸âååï¼ä¸å½èµäº§æäºå
¨çèµéççé¨âæå¡âæ¿åï¼æ¸¯è¡ã䏿¦è¡é¢è·å
¨çã\nãã转æç¹å¨4æ19æ¥å¨äºæé´ï¼ä¸å½è¯çä¼åå¸5é¡¹èµæ¬å¸åºå¯¹æ¸¯å使ªæ½ï¼æ¨å¨æå±ä¼åæ²ªæ·±æ¸¯éæºå¶ï¼å©å馿¸¯å·©åºæåå½é
éèä¸å¿å°ä½ã次å¨ä¸4æ22æ¥èµ·ï¼æ¸¯è¡ä¸»è¦ææ°ãç¾è¡ä¸æ¦è¡è¿ç»é¼ç©ºå¤§æ¶¨ã4æ23æ¥ï¼çé¶å°ä¸å½å¤§éåä¸å½é¦æ¸¯è¡ç¥¨è¯çº§ä¸è°è³è¶
é
ï¼ä¸ºè¿å¹´æ¥å¤èµå¤§è¡ç½è§å±å¤ä¸å½èµäº§ã\nãã5æ2æ¥ï¼ä¸èµè¡è¡æ
å¼çã\nããåçäºâäºä¸âæé´ä¸å½ç»æµå¤èéªè¯çä¹è§å¤æï¼å
¨çèµæ¬å éæµå
¥é¦æ¸¯å¸åºï¼æ¸¯è¡ç§ææ¿åå
¨çº¿æåãæçç§æææ°ï¼HSTECH.HIï¼ååº¦ææ¶¨4.45%ï¼æç馿¸¯ä¸å¸çç©ç§æææ°ï¼HSHKBIO.HIï¼æ´æ¯æ´æ¶¨4.7%ï¼ç¾è¡ä¸æ¦è¡ææ°ï¼US36966ï¼ä¹å¤§æ¶¨6.96%ãä»4æ22æ¥å°5æ6æ¥ï¼æçææ°ç½è§è¿ç»10ä¸ªäº¤ææ¥æ¶æ¶¨ï¼ç´¯è®¡ä¸æ¶¨14.51%ï¼é¢æâææ¯æ§çå¸âä¹å¿ã\n\nãã馿¸¯å¸åºæ ¸å¿ææ°è¡¨ç°ï¼æ°æ®æ¥æºï¼Windï¼\nããæç馿¸¯ä¸å¸çç©ç§æææ°æ¯æ¸¯è¡çç©å¶è¯ãåºå æµåºãç»èæ²»ççé¢åï¼é«ç§æã髿é¿ãé«å¼¹æ§çä»£è¡¨ææ°ï¼æä»½è¡å
æ¬å¨é¦æ¸¯äº¤ææéè¿ä¸å¸è§å第18Aç« ä¸å¸çå
¬å¸ï¼åæ°è¯ãCXOçåæ²¿ç§æå«é尤为çªåºãæ¤é´å
¶å¨æ¸¯è¡æ ¸å¿ææ°ä¸ä¸æç¬ç§ç表ç°ï¼è¡¨æå
¨çèµéå¨é£é©å好æåæ¶å¯¹æ¸¯è¡å»è¯çç©æ¿åçç¹å«åéã\nããè¿æ¯ä¸ä¸ªå¼å¾é«åº¦å
³æ³¨çæ°å¨åï¼æ¸¯è¡å»è¯çç©æ¿åæè¿æ¥ä¸æå¸å±çæç¹çªå£ã\n\nããä¸å¹´éè¡è°æ´æè§åº\nããå¤ççï¼æç馿¸¯ä¸å¸çç©ç§æææ°äº2019å¹´12æ16æ¥åå¸ã2021å¹´6æ29æ¥æ¸é¡¶å岿é«ç¹2925.74ç¹ï¼å°2024å¹´4æ19æ¥è§¦åºæä½ç¹712.69ç¹ãæé´åç»è¿ä¸å¹´å·¨å¹
éè¡è°æ´ï¼å岿ç¹çæå¤§è°æ´å¹
度é«è¾¾75.64%ã\n\nããï¼æ°æ®æ¥æºï¼Windï¼æ¶é´ï¼2019.12.16~2024.5.8ï¼\nããæ¸¯è¡çç©ç§æææ°å叿¶æ£å¼æ°å ç«æ
çåï¼çç©å»è¯æ¿åå¨éæ±æ¨å¨ä¸æç»æé«ä¼°å¼ï¼ææ°èªå叿¥èµ·ï¼ççä¸å¹´åæ¶é´å
ï¼å°2021å¹´6æ28æ¥æ¶çä»·å岿髿¥æ¶ï¼è¶³è¶³ä¸æ¶¨äº96.54%ãåºæ¬é¢ä»·å¼å¨è¾çæ¶é´å
被æåéæ¯æ¯ææ°è§é¡¶çéè¦åå ã\nããè§é¡¶æ·±è·çå¤é¨æ¨å¨å ç´ ï¼åæ¯ç¾å½å¯¹ä¸ç¾ç§æäº¤æµçæ¿ç转åã2021å¹´5æ12æ¥ï¼ç¾å½å½ä¼åè®®é¢åå¡å§åä¼éè¿âæ 尽忲¿æ³æ¡âï¼æ¹åå¨5å¹´å
æ¨æ¬¾1100å¤äº¿ç¾å
ç¨äºæèµäººå·¥æºè½ãå导ä½ãéå计ç®ãå
è¿éä¿¡ãçç©ææ¯åå
è¿è½æºçå
³é®ææ¯é¢åçåºç¡åé«çº§ç ç©¶ãåä¸å以åæè²åå¹è®è®¡åï¼ä»¥åºå¯¹æ¥èªä¸å½æ¥ç严éçç«äºååãå¾å¿«ï¼6æ8æ¥ï¼ç¾åè®®é¢éè¿å级ççã2021ç¾å½åæ°åç«äºæ³æ¡ãï¼æèµé颿åå°2500亿ç¾å
ã\nããèªé«ç¹åä¸ï¼çç©ç§æææ°æç»åè¾¹ä¸æ«è¿ä¸å¹´æ¶é´æå¼å¯éè¡æ´çã2024å¹´1æï¼ç¾å½åæãçç©å®å
¨æ³æ¡ãï¼ä¸å½åæ°è¯ãCXOä¼ä¸åéå²å»ï¼çç©ç§æææ°å¼å§éæ¥è§¦åºã\nããWindæ°æ®æ¾ç¤ºï¼æªè³2024å¹´5æ8æ¥ï¼æç馿¸¯ä¸å¸çç©ç§æææ°å¨è¿ç»åå¼¹ä¹åï¼å¸ççTTMä¹ä»
为28.64åï¼å¤äºåå²åä½ç¹2.59%çæä½ä½ç½®ï¼è¿ä½äºåå²ä¸ä½å¼ç90.9åãæå¤§å¼ç2126.3åï¼å¸åç1.72åï¼å¤äºåå²åä½ç¹3.93%ï¼å¸éç0.85åï¼å¤äºåå²åä½ç¹3.49%ï¼ä¿®å¤ç©ºé´å·¨å¤§ï¼å½å估弿§ä»·æ¯ãå®å
¨è¾¹é
åè¾é«ã\n\nããåæ°è¯è·å
¨æ¹ä½æ¯æ\nããå¨å¸åºæ
ç»ªè½¬ææ¶ï¼å»è¯çç©æ¹ååå°èµéå好ç大é»è¾æ¯ï¼èé¾åå éèæ¯ä¸ï¼å»ç弿¯å¨éæ±ç«¯æ¯è¾ä¸ºåæ§çã4æä¸æ¬ï¼é对沪深港å¸åºçç©ç§æãå»è¯æ¿åè¡æ
ï¼ä¸æµ·è¯å¸åæï¼éæ±ç«¯åä¾ç»ç«¯6大å ç´ å°å
±åæ¨å¨æ¶è´¹å»çå¸åºå¢é¿ã\nããéæ±ç«¯ï¼ï¼1ï¼æ¿ç红å©éæ¾ãâå¥åº·ä¸å½2030âå¥å
¨å
¨æ°å¥åº·å¶åº¦ä½ç³»ï¼æ¨è¿ä»¥âæ²»ç
为ä¸å¿âåâ以å¥åº·ä¸ºä¸å¿â转åãï¼2ï¼æ¯ä»è½åæåã对æ åè¾¾å½å®¶ç»éªï¼ä¸å½å¥åº·äº§ä¸æ¯ä»è½å仿è¾å¤§æå空é´ãï¼3ï¼äººå£ç»æååãéçèé¾åå éï¼å
·ææ´å¼ºç»æµå®åä¸æè²æ°´å¹³çâ60åâæ¥å
¥é伿½®ï¼å»çå¥åº·è¿æ¥æºçéæ±ãï¼4ï¼åç«æ
æ¶ä»£æ´å å
³æ³¨å¥åº·ã大伿¥å¸¸ä¿å¥æè¯æåï¼è®©ä¿å
»çææä¸ºå¯æç»çæ¥å¸¸ã\nããä¾ç»ç«¯ï¼ï¼1ï¼äº§å转å忰驱å¨ï¼åææåä¸å éè¿ä»£ï¼æçå¤å
ååå±ï¼æ»¡è¶³æ¶è´¹è
夿 ·åéæ±ã2023å¹´ä¿å¥é£å夿¡3632款ï¼åæ¯å¢é¿52%ãï¼2ï¼æ°ååå»ççæå¸å±æ¥è»å®åï¼ææé¾æ¥å¥åº·æ¶è´¹ç¾¤ä½ï¼å
æ¬ä¿¡æ¯ç§æ®ãè¿ç¨é®è¯ã线ä¸è´è¯ãç¾ç
管çãå
¶ä¸ä¿¡æ¯ç§æ®éè¿å类社交平å°ãæç´¢å¼æçæä¾ä¿å¥å
»çãç¾ç
è¯æ²»çç¸å
³ä¿¡æ¯ï¼æ¸éç大äº60%ã\nããè¿æ¥ï¼å¨å¤é¨ååä¸ï¼åæ°è¯è¡ä¸éæ¥é
é
¿å
¨æ¹ä½æ¯ææ¿çãä»å¹´3æâåæ°è¯â馿¬¡åå
¥å½å¡é¢æ¿åºå·¥ä½æ¥åï¼å
·ä½æ¯ææ¿çæ£æ¸æ¬¡æ¨åºã4æ7æ¥ï¼å京å¸å»çä¿éå±ç9é¨é¨å¶å®ãåäº¬å¸æ¯æåæ°å»è¯é«è´¨éåå±è¥å¹²æªæ½ï¼2024ï¼ï¼å¾æ±æè§ç¨¿ï¼ãã4æ11æ¥ï¼å½å®¶å»ä¿å±åå¸ã2023å¹´å»çä¿éäºä¸åå±ç»è®¡å¿«æ¥ãï¼éè¿å»ºç«è¦çç³æ¥ãè¯å®¡ãæµç®ãè°å¤çå
¨æµç¨çåæ°è¯æ¯ææºå¶ï¼æ¯ææ´å¤æ°è¯å¥½è¯çº³å
¥å»ä¿ï¼2023ï¼å¹´ç®å½è°æ´ä¸ï¼æï¼57ï¼ä¸ªè¯åå®ç°äºâå½å¹´è·æ¹ãå½å¹´çº³å
¥ç®å½âã\nããåæ°è¯ä½ä¸ºæ°è´¨ç产å代表ï¼âæ¿çå
âæ¥ç»åºå°ï¼å»è¯äº§ä¸è¿æ¥æ·±å»åé©ï¼å¸åºå¯¹å»è¯æ¿åãåæ°è¯äº§ä¸çæ²è§é¢æä¹æ£å¨æ ¹æ¬æè½¬ã\nããäºå®ä¸ï¼2022å¹´~2023å¹´å
¨çè¯ä¼ç®¡çº¿è§æ¨¡æåå25å䏿3ï¼å®¶ä¸å½ä¼ä¸ä¸æ¦ï¼è¡¨ææå½å¨å
¨çè¯ç©åæ°ä¸å´é²å¤´è§ãâååäºâæé´ï¼æå½åæ°è¯æ£ç§¯æå®ç°é«è´¨éâå¼è¿æ¥âå髿°´å¹³âèµ°åºå»âã2021å¹´~2022ï¼å¹´ï¼ä¸å½åæ°è¯ææå¼è¿ï¼license-inï¼äº¤æé¡¹ç®å¤æ¶åCDååãRNAè¯ç©çèµéãåæ¶ï¼ç¾æµç¥å·çæ³½å¸æ¿å°¼å®ç°äºæ¬åæ°è¯åºæµ·âé¶ççªç ´âï¼ä¸å¯¹å¤ææï¼license-outï¼ä¹æ·æ¥é¢é¢ãæ®å»è¯éæ¹æ°æ®ç»è®¡ï¼2022ï¼å¹´ï¼æå½license-outäº¤ææ»éé¢è¾¾277亿ç¾å
ï¼è¾2020å¹´å¢é¿122.08%ãï¼æ¥æºï¼ãä¸å½å»è¯äº§ä¸é«è´¨éåå±ç¶åµè°ç ï¼2021â2023ï¼ãç ç©¶æ¥å综述ï¼ä¸å½é£åè¯åç管æå¿2024å¹´ç¬¬ä¸æï¼\nããå½éè¯å¸è®¤ä¸ºï¼å
¨çåæ°è¯æ¯ç§æè¿æ¥å
ç°çéè¦é¢åï¼ä¸å½åæ°è¯äº§ä¸é¾æ£å¨å´èµ·ï¼é¢å
è¯çå
¨é¢æ¢å¤ï¼å½å®¶å¯¹åæ°è¯çé¼å±æ¯ææ¿çéç»åºå°ï¼ä»¥åä¸å½è¯ä¼å½é
忥ä¼çæ¨è¿ï¼è¡ä¸æ´ä½å°è¿æ¥ä¸ç»©ä¸å¸åºè¡¨ç°ç忥åä¸ãä¸é¶è¯å¸è¡¨ç¤ºï¼å»è¯çç©è¡ä¸ä¼´éâå»è¯åè
â影忶éååºæ°ååæ¶åï¼åç»ä¸ç»©å¢éææé壿¹åï¼å½å
æ¿çæ¯æååæ°è¯åºæµ·åæ¯åå¼å¾æå¾
ã\n\nããç¾éæ¯å¨æå©å¥½ç§æ\nããä»å¤é¨ç¯å¢èè¨ï¼è¿ææ¸¯è¡æç»ä¸æ¶¨ï¼æåç¾å
鿝卿æ¥ä¸´ãæ¥æ¬ç»§ç»å æ¯æ¦çéä½çå½±åï¼å¤èµé¨åæµå港è¡å¸åºã\nãã卿¸¯è¡5ææèµçç¥ä¸ï¼å½ä¿¡è¯å¸è®¤ä¸ºï¼æ¸¯è¡ææåè²é¿æåºæ¬é¢ä¿®å¤è¡æ
ãéçæçç§æå¤å¹´æ¥ä¼°å¼åºé¨çç¡®è®¤ï¼æçææ°ãæç港è¡éãæçé«è¡æ¯ãæçç§æçä¼°å¼åºé¨å
¨é¨ç¡®è®¤å®æ¯ï¼å°ææ¶ä¸ç´ä»¥æ¥âæä¼°å¼âçæå¤§ä¸ç¡®å®æ§ï¼æå©äºé¿çº¿èµéåæµè¿äºå
¬å¸å¹¶å¼å¯é¿æåºæ¬é¢ä¿®å¤ã\nãã彿³°åå®ç æ¥è¡¨ç¤ºï¼æ¸¯è¡å¨è¿æå¤§å¹
èµ°é«åï¼ä¼åºç°ä¸å®åæ§åéè¡ï¼ä½éçä¸å½å¨ç»æµãæ¿çé¢åä¸ç¡®å®æ§éä½ï¼æ¸¯è¡ä»æå¯ä¸ºç©ºé´ãå½å
æ¹é¢ï¼ç»æµæ¿ç以稳为主ï¼å°äº§æ¿çæç»ä¼åï¼æ¸¯è¡å¸åºçå©é¢ææ¹åï¼å¤èµä¸ä¿®å½å
ç»æµåæ¯ï¼æ¸¯è¡å¸åºæ
绪修å¤ãæµ·å¤æ¹é¢ï¼ç¾èå¨åµæ¤éèåå³å¨åå¸åºçæå¾ææ¾ï¼å¸åºæµå¨æ§è¿ä¸æ¥æ¶ç´§çé£é©ä¸éã\nããèå»è¯çç©ãåæ°è¯ãCXOäº§ä¸æ¹åï¼é¤äºå
³æ³¨åºæ¬é¢è¶å¿ï¼ä½ä¸ºç§æä¸»çº¿ï¼ä¼°å¼è¿åå°å
¨çèµéæµå¨ï¼å°¤å
¶ç¾å
å¨æçæ·±å»å½±åãè¿æï¼è½ç¶ç¾èå¨éæ¯é¢æææå»¶åï¼ä½ç¾å
鿝卿å¼å¯çå¤æå¹¶æªæ¹åï¼å¯¹ç§æåæ°ä¸»çº¿çä¼°å¼å½¢ææå©çå
¨çæµå¨æ§è¾¹é
æ¹åç¯å¢ã\nããåæ°ä¸»çº¿æ¯ç©¿è¶å¨æçæ³å®ãçç©ç§ææ¯å»è¯è¡ä¸åæ°åº¦é«ãæé¿ç©ºé´å¤§ç主æµèµéãæç馿¸¯ä¸å¸çç©ç§æææ°å·²åºæ¬å®ææ¿çåºãåºæ¬é¢åºãä¼°å¼åºãèµéé¢åºçå
±æ¯ï¼éåè¿è¡ä¸é¿å¨æå¸å±ææï¼å享å»è¯çç©é«æé¿æ ¸å¿èµäº§çå¤å©å¢é¿ã\nããè¿æä»½é¢æç»å¢é¿ãå弿æ¾ä¿®å¤ãåºå
ä»·æ ¼æç»åå¼¹çæçå»è¯ETFï¼159892.SZï¼ï¼æ£æ¯è·è¸ªæç馿¸¯ä¸å¸çç©ç§æææ°çé«å
åç§ï¼å¨è·è¸ªåææ°çETF产åä¸ï¼æçå»è¯ETFçåºéè§æ¨¡ãæäº¤é¢æå¤§ï¼æµå¨æ§è¯å¥½ä¸æ¯æT+0交æãåæ¶ï¼æçå»è¯ETF乿坹åºçåºå¤èæ¥åºéï¼Aç±»ï¼016970ï¼Cç±»ï¼016971ï¼ï¼è´¹çå¨åç±»ä¸å±æä½æ¡£æ°´å¹³ï¼æ²¡æè¡ç¥¨è´¦æ·çåºå¤æèµè
ä¹å¯ä»¥å
³æ³¨ã\nããå¼å¾ä¸æçæ¯ï¼æ¸¯è¡å»è¯çç©æ¿åå ä¸å¸å¶åº¦å·®å¼ï¼å
å«äºä¸äºæªè½å¨Aè¡ä¸å¸çä¼è´¨ç¨ç¼ºççç©å»è¯ä¼ä¸ï¼æç馿¸¯ä¸å¸çç©ç§æææ°åå大æåè¡ä¸ç»å¤§é¨å齿²¡æå¨Aè¡ä¸å¸ãè¿ä¹æ¯æ¸¯è¡å»è¯çç©æ¿ååºå«Aè¡çæ ¸å¿èµäº§ä¼å¿ï¼è¯¥ææ°åå大æåè¡æ¶µçå个ç»åé¢åçé¾å¤´ä¼ä¸ï¼è¦ççç©å»è¯äº§ä¸é¾ä¸ä¸ä¸æ¸¸å个ç»åç¯èã\nããæ»ä¹ï¼éè¿æçå»è¯ETF䏿½åä¹°å
¥äºå¨æ¸¯ä¸å¸ç䏿µæ°èå®åè¯ä¼ï¼å¨å¨ææç¹å¸å±æå¯æ¥æè¾å¼ºçç©¿è¶å¨æè½åã\n\n责任ç¼è¾ï¼æ¨çº¢å\n\nVIPè¯¾ç¨æ¨è\næ°æµªç´æ\n\n@@title@@\nAPPä¸äº«ç´æ\nç鍿¨è\n\n24å°æ¶æ»å¨ææ¥ææ°çè´¢ç»èµè®¯åè§é¢ï¼æ´å¤ç²ä¸ç¦å©æ«æäºç»´ç å
³æ³¨ï¼sinafinanceï¼\nè¡å¸ç´æ\n7X24å°æ¶\n\næ°æµªè´¢ç»æè§åé¦çè¨æ¿\næ°æµªç®ä»|广åæå¡|About Sina\nèç³»æä»¬|æèä¿¡æ¯|éè¡è¯æ³¨å\n产åçç|ç½ç«å¾å¸|SINA English\nCopyright © 1996-2024 SINA Corporation\nAll Rights Reserved æ°æµªå
¬å¸ çæææ",
- "images": [
-
- ]
- },
- {
- "url": "https://m.lejucaijing.com/news-7355823170131785420.html?&originflag=commonshare",
- "raw_content": "热门搜索\n搜索历史清空\n创新药持续大涨,恒生医药ETF再创新高,多只个股翻倍\n有连云 2025-07-29 12:52 4249阅读\n7月29日早盘,港股医药延续上涨行情,恒生医药ETF(159892)盘中涨超1%,2025年涨幅超86%,在全市场ETF中涨幅靠前;其持仓股三生制药年内涨幅超400%,诺诚健华、乐普生物、信达生物等年内涨超100%。\n消息面来看,国家育儿补贴制度实施方案7月28日公布。从2025年1月1日起,对符合法律法规规定生育的3周岁以下婴幼儿发放补贴,至其年满3周岁。育儿补贴按年发放,现阶段国家基础标准为每孩每年3600元。\n7月28日,恒瑞医药公告与生物制药公司GSK达成协议,将HRS-9821项目的全球独家权利和至多11个项目的全球独家许可的独家选择权有偿许可给GSK。GSK将向恒瑞支付5亿美元的首付款,里程碑付款的潜在总金额约120亿美元。\n来源:有连云\n\n《乐居财经精选》\n民营房企反扑\n\n相关文章\n24小时热门文章\n最新文章\n评论\n点击下载App参与更多互动\n前往乐居财经APP查看原文,体验更佳\n关注\n原创\n数据\n榜单",
- "images": [
-
- ]
- },
- {
- "url": "https://popcj.com/depth/0204832508379321",
- "raw_content": "港股医药为何尾盘暴涨?原因找到了\n2025-08-05 17:42:24\n港股医药板块尾盘大幅拉升,恒生生物科技指数收盘涨3.86%,在全市场指数中领先,跟踪该指数的恒生医药ETF(159892)明日或有补涨可能。\n具体到个股来看,君实生物(01877.HK)尾盘飙升,一度近35%。君实生物此前已于6月20日完成配售4100万股新股事项,本次配售的募集资金净额约为10.26亿港元。公司拟将配售募集资金净额的70%用于创新药研发。\n乐普生物大涨超14%,MRG004A启动III期临床,MRG007完成首例患者入组,MRG007近期拿下4700万美元的授权交易(首付款+近期里程碑付款)。\n国金证券此前表示,全球双抗开发热情高涨,国产抗体行业迎来创新成果兑现窗口期。君实生物对单抗开发具有丰富经验,其开发的PD-1单抗分子特瑞普利是最早上市的国产PD-1抑制剂,目前特瑞普利在国内上市众多适应症,同时持续拓展全球商业化网络。\n免责声明:所有平台仅提供服务对接功能,资讯信息、数据资料来源于第三方,其中发布的文章、视频、数据仅代表内容发布者个人的观点,并不代表泡财经平台的观点,不构成任何投资建议,仅供参考,用户需独立做出投资决策,自行承担因信赖或使用第三方信息而导致的任何损失。投资有风险,入市需谨慎。\n\n请先登录后发表评论\n共0条评论\n扫码在手机上查看\n相关文章\n诚志股份(000990):上半年归母净利润同比降低89.78%\n\n阳光电源(300274):中标晋能控股晋北能源(山西)有限公司采购项目,中标金额为4.06亿元\n\n苏豪弘业(600128):中标上海市长宁区妇幼保健院采购项目,中标金额为1438.74万元\n\n联系我们| 媒体合作 popcj@gudongtech.com| 投诉建议 popcj@gudongtech.com\n粤ICP备2020093236号-3| 粤ICP备2020093236号-4| 用户协议| 隐私政策| 网站地图\n郑重声明:古东管家所有平台仅提供服务对接功能,其中发布的文章、视频、数据仅代表内容发布者个人的观点,与古东管家平台的立场无关,不对您构成任何投资建议,仅供参考。用户需独立做出投资决策,风险自担。投资有风险,选择需谨慎。\n版权所有 © 深圳市古东管家科技有限责任公司\n\n扫码下载古东管家APP",
- "images": [
-
- ]
- }
- ],
- "failed_results": [
- {
- "url": "https://guba.eastmoney.com/news,sz159892,1544579467.html",
- "error": "Failed to fetch content"
- }
- ],
- "response_time": 3.72
-}
\ No newline at end of file
diff --git a/memoryscope/Dockerfile b/memoryscope/Dockerfile
new file mode 100644
index 00000000..68f4ab1d
--- /dev/null
+++ b/memoryscope/Dockerfile
@@ -0,0 +1,55 @@
+# __ __ ____
+# | \/ | ___ _ __ ___ ___ _ __ _ _/ ___| ___ ___ _ __ ___
+# | |\/| |/ _ \ '_ ` _ \ / _ \| '__| | | \___ \ / __/ _ \| '_ \ / _ \
+# | | | | __/ | | | | | (_) | | | |_| |___) | (_| (_) | |_) | __/
+# |_| |_|\___|_| |_| |_|\___/|_| \__, |____/ \___\___/| .__/ \___|
+# |___/ |_|
+
+# Instruction
+
+# To construct docker image:
+# sudo docker build --network=host -t memoryscope .
+
+# To run docker image:
+# sudo docker run -it --rm --memory=4G --net=host memoryscope
+# To run docker image with arguments (refer to memoryscope/core/config/arguments.py):
+# sudo docker run -it --rm --memory=4G --net=host -e "OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -e "language=en" -e "human_name=superman" -e "generation_backend=openai_generation" -e "generation_model=gpt-4o" -e "embedding_backend=openai_embedding" -e "embedding_model=text-embedding-3-small" -e "enable_ranker=False" memoryscope
+
+FROM python:3.11
+
+# (Not necessary) Change pip source
+RUN echo '[global]' > /etc/pip.conf && \
+ echo 'index-url = https://mirrors.aliyun.com/pypi/simple/' >> /etc/pip.conf && \
+ echo 'trusted-host = mirrors.aliyun.com' >> /etc/pip.conf
+
+# Install Elastic Search
+RUN useradd -m elastic_search_user
+USER elastic_search_user
+WORKDIR /home/elastic_search_user/elastic_search
+# COPY elasticsearch-8.15.0-linux-x86_64.tar.gz ./elasticsearch-8.15.0-linux-x86_64.tar.gz
+RUN wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.15.0-linux-x86_64.tar.gz
+RUN tar -xzf elasticsearch-8.15.0-linux-x86_64.tar.gz
+WORKDIR /home/elastic_search_user/elastic_search/elasticsearch-8.15.0
+ENV DISCOVERY_TYPE=single-node \
+ XPACK_SECURITY_ENABLED=false \
+ XPACK_LICENSE_SELF_GENERATED_TYPE=trial
+
+# Change user back to root and fix ownership
+USER root
+RUN chown -R elastic_search_user:elastic_search_user /home/elastic_search_user/
+WORKDIR /memory_scope_project
+
+# (Not necessary) Install the majority of deps, using docker build cache to accelerate future building
+COPY requirements.txt ./
+RUN pip3 install -r requirements.txt
+
+# Enter working dir
+WORKDIR /memory_scope_project
+COPY . .
+# RUN pip3 install poetry
+# RUN poetry install
+RUN pip3 install -r requirements.txt
+
+# Launch!
+# CMD ["bash"]
+CMD ["bash", "examples/docker/entrypoint.sh"]
\ No newline at end of file
diff --git a/memoryscope/DockerfileArm b/memoryscope/DockerfileArm
new file mode 100644
index 00000000..6166f0fa
--- /dev/null
+++ b/memoryscope/DockerfileArm
@@ -0,0 +1,56 @@
+# __ __ ____
+# | \/ | ___ _ __ ___ ___ _ __ _ _/ ___| ___ ___ _ __ ___
+# | |\/| |/ _ \ '_ ` _ \ / _ \| '__| | | \___ \ / __/ _ \| '_ \ / _ \
+# | | | | __/ | | | | | (_) | | | |_| |___) | (_| (_) | |_) | __/
+# |_| |_|\___|_| |_| |_|\___/|_| \__, |____/ \___\___/| .__/ \___|
+# |___/ |_|
+
+# Instruction
+
+# To construct docker image:
+# sudo docker build --network=host -t memoryscope .
+
+# To run docker image:
+# sudo docker run -it --rm --memory=4G --net=host memoryscope
+# To run docker image with arguments (refer to memoryscope/core/config/arguments.py):
+# sudo docker run -it --rm --memory=4G --net=host -e "OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -e "language=en" -e "human_name=superman" -e "generation_backend=openai_generation" -e "generation_model=gpt-4o" -e "embedding_backend=openai_embedding" -e "embedding_model=text-embedding-3-small" -e "enable_ranker=False" memoryscope
+#docker run -it --rm ghcr.io/modelscope/memoryscope_arm /bin/bash
+FROM python:3.11
+
+# (Not necessary) Change pip source
+RUN echo '[global]' > /etc/pip.conf && \
+ echo 'index-url = https://mirrors.aliyun.com/pypi/simple/' >> /etc/pip.conf && \
+ echo 'trusted-host = mirrors.aliyun.com' >> /etc/pip.conf
+
+# Install Elastic Search
+RUN useradd -m elastic_search_user
+USER elastic_search_user
+WORKDIR /home/elastic_search_user/elastic_search
+RUN wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.15.2-linux-aarch64.tar.gz
+RUN tar -xzf elasticsearch-8.15.2-linux-aarch64.tar.gz
+RUN mv /home/elastic_search_user/elastic_search/elasticsearch-8.15.2 /home/elastic_search_user/elastic_search/elasticsearch-8.15.0
+WORKDIR /home/elastic_search_user/elastic_search/elasticsearch-8.15.0
+ENV DISCOVERY_TYPE=single-node \
+ XPACK_SECURITY_ENABLED=false \
+ XPACK_LICENSE_SELF_GENERATED_TYPE=trial
+
+# Change user back to root and fix ownership
+USER root
+RUN chown -R elastic_search_user:elastic_search_user /home/elastic_search_user/
+WORKDIR /memory_scope_project
+
+# (Not necessary) Install the majority of deps, using docker build cache to accelerate future building
+COPY requirements.txt ./
+RUN pip3 install -r requirements.txt
+
+# Enter working dir
+WORKDIR /memory_scope_project
+COPY . .
+# RUN pip3 install poetry
+# RUN poetry install
+RUN pip3 install -r requirements.txt
+
+# Launch!
+# CMD ["bash"]
+CMD ["bash", "examples/docker/entrypoint.sh"]
+
diff --git a/memoryscope/LICENSE b/memoryscope/LICENSE
new file mode 100644
index 00000000..e1dea3fe
--- /dev/null
+++ b/memoryscope/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 Alibaba Group
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/memoryscope/README.md b/memoryscope/README.md
new file mode 100644
index 00000000..f2e7143c
--- /dev/null
+++ b/memoryscope/README.md
@@ -0,0 +1,131 @@
+English | [**中文**](./README_ZH.md) | [**日本語**](./README_JP.md)
+
+# MemoryScope
+
+
+
+Equip your LLM chatbot with a powerful and flexible long term memory system.
+
+[](https://pypi.org/project/memoryscope/)
+[](https://pypi.org/project/memoryscope/)
+[](./LICENSE)
+[](https://modelscope.github.io/MemoryScope/en/index.html#welcome-to-memoryscope-tutorial)
+[](https://modelscope.github.io/MemoryScope/en/docs/api.html)
+[](https://modelscope.github.io/MemoryScope/en/docs/contribution.html)
+
+----
+## 📰 News
+
+- **[2024-09-10]** We release MemoryScope v0.1.1.0 now, which is also available in [PyPI](https://pypi.org/simple/memoryscope/)!
+----
+## 🌟 What is MemoryScope?
+MemoryScope provides LLM chatbots with powerful and flexible long-term memory capabilities, offering a framework for building such abilities.
+It can be applied to scenarios like personal assistants and emotional companions, continuously learning through long-term memory to remember users' basic information as well as various habits and preferences.
+This allows users to gradually experience a sense of "understanding" when using the LLM.
+
+### Demo
+
+
+
+
+### Framework
+
+
+
+
+💾 Memory Database: MemoryScope is equipped with a vector database (default is *ElasticSearch*) to store all memory fragments recorded in the system.
+
+🔧 Worker Library: MemoryScope atomizes the capabilities of long-term memory into individual workers, including over 20 workers for tasks such as query information filtering, observation extraction, and insight updating.
+
+🛠️ Operation Library: Based on the worker pipeline, it constructs the operations for memory services, realizing key capabilities such as memory retrieval and memory consolidation.
+
+- Memory Retrieval: Upon arrival of a user query, this operation returns the semantically related memory pieces
+and/or those from the corresponding time if the query involves reference to time.
+- Memory Consolidation: This operation takes in a batch of user queries and returns important user information
+extracted from the queries as consolidated *observations* to be stored in the memory database.
+- Reflection and Re-consolidation: At regular intervals, this operation performs reflection upon newly recorded *observations*
+to form and update *insights*. Then, memory re-consolidation is performed to ensure contradictions and repetitions
+among memory pieces are properly handled.
+
+
+⚙️ Best Practices:
+
+- Based on the core capabilities of long-term memory, MemoryScope has implemented a dialogue interface (API) with long-term memory and a command-line dialogue practice (CLI) with long-term memory.
+- MemoryScope combines currently popular agent frameworks (AutoGen, AgentScope) to provide best practices.
+
+### Main Features
+
+⚡ Low response-time (RT) for the user:
+- Backend operations (Memory Consolidation, Reflection and Re-consolidation) are decoupled from the frontend operation
+ (Memory Retrieval) in the system.
+- While backend operations are usually (and are recommended to be) queued or executed at regular intervals, the
+system's response time (RT) for the user depends solely on the frontend operation, which is only ~500ms.
+
+🌲 Hierarchical and coherent memory:
+- The memory pieces stored in the system are in a hierarchical structure, with *insights* being the high level information
+from the aggregation of similarly-themed *observations*.
+- Contradictions and repetitions among memory pieces are handled periodically to ensure coherence of memory.
+- Fictitious contents from the user are filtered out to avoid hallucinations by the LLM.
+
+⏰ Time awareness:
+- The system is time sensitive when performing both Memory Retrieval and Memory Consolidation. Therefore, it can retrieve
+accurate relevant information when the query involves reference to time.
+
+----
+
+## 💼 Supported Model API
+
+| Backend | Task | Some Supported Models |
+|-------------------|------------|------------------------------------------------------------------------|
+| openai_backend | Generation | gpt-4o, gpt-4o-mini, gpt-4, gpt-3.5-turbo |
+| | Embedding | text-embedding-ada-002, text-embedding-3-large, text-embedding-3-small |
+| dashscope_backend | Generation | qwen-max, qwen-plus, qwen-plus, qwen2-72b-instruct |
+| | Embedding | text-embedding-v1, text-embedding-v2 |
+| | Reranker | gte-rerank |
+
+In the future, we will support more model interfaces and local deployment of LLM and embedding services.
+
+
+## 🚀 Installation
+For installation, please refer to [Installation.md](docs/installation.md).
+
+
+## 🍕 Quick Start
+- [Simple Usages (Quick Start)](./examples/api/simple_usages.ipynb)
+- [With AutoGen](./examples/api/autogen_example.md)
+- [CLI with a MemoryScope Chatbot](./examples/cli/CLI_README.md)
+- [Advanced Customization](./examples/advance/custom_operator.md)
+
+## 💡 Contribute
+
+Contributions are always encouraged!
+
+We highly recommend install pre-commit hooks in this repo before committing pull requests.
+These hooks are small house-keeping scripts executed every time you make a git commit,
+which will take care of the formatting and linting automatically.
+```shell
+pip install -e .
+pre-commit install
+```
+
+Please refer to our [Contribution Guide](./docs/contribution.md) for more details.
+
+## 📖 Citation
+
+Reference to cite if you use MemoryScope in a paper:
+
+```
+@software{MemoryScope,
+author = {Li Yu and
+ Tiancheng Qin and
+ Qingxu Fu and
+ Sen Huang and
+ Xianzhe Xu and
+ Zhaoyang Liu and
+ Boyin Liu},
+month = {09},
+title = {{MemoryScope}},
+url = {https://github.com/modelscope/MemoryScope},
+year = {2024}
+}
+```
diff --git a/memoryscope/README_JP.md b/memoryscope/README_JP.md
new file mode 100644
index 00000000..8186eb90
--- /dev/null
+++ b/memoryscope/README_JP.md
@@ -0,0 +1,121 @@
+[**English**](./README.md) | [**中文**](./README_ZH.md) | 日本語
+
+# MemoryScope
+
+
+
+あなたのLLMチャットボットに強力で柔軟な長期記憶システムを装備しましょう。
+
+[](https://pypi.org/project/memoryscope/)
+[](https://pypi.org/project/memoryscope/)
+[](./LICENSE)
+[](https://modelscope.github.io/MemoryScope/en/index.html#welcome-to-memoryscope-tutorial)
+[](https://modelscope.github.io/MemoryScope/en/docs/api.html)
+[](https://modelscope.github.io/MemoryScope/en/docs/contribution.html)
+
+----
+## 📰 ニュース
+
+- **[2024-09-10]** MemoryScope v0.1.1.0をリリースしました。 [PyPI](https://pypi.org/simple/memoryscope/)でも入手可能です!
+----
+## 🌟 MemoryScopeとは?
+MemoryScopeは、LLMチャットボットに強力で柔軟な長期記憶能力を提供し、その能力を構築するためのフレームワークを提供します。
+個人アシスタントや感情的な伴侶などのシナリオに適用でき、長期記憶を通じてユーザーの基本情報やさまざまな習慣や好みを覚え続けることができます。
+これにより、ユーザーはLLMを使用する際に徐々に「理解されている」感覚を体験することができます。
+
+### デモ
+
+
+
+
+### フレームワーク
+
+
+
+
+💾 メモリデータベース: MemoryScopeは、システム内に記録されたすべての記憶片を保存するためのベクトルデータベース(デフォルトは*ElasticSearch*)を備えています。
+
+🔧 ワーカーライブラリ: MemoryScopeは、長期記憶の能力を個々のワーカーに原子化し、クエリ情報のフィルタリング、観察の抽出、洞察の更新など、20以上のワーカーを含みます。
+
+🛠️ オペレーションライブラリ: ワーカーパイプラインに基づいて、メモリサービスのオペレーションを構築し、メモリの取得やメモリの統合などの主要な機能を実現します。
+
+- メモリの取得: ユーザークエリが到着すると、この操作は意味的に関連する記憶片を返します。
+ クエリが時間に言及している場合は、対応する時間の記憶片も返します。
+- メモリの統合: この操作は、一連のユーザークエリを受け取り、クエリから抽出された重要なユーザー情報を統合された*観察*としてメモリデータベースに保存します。
+- 反映と再統合: 定期的に、この操作は新たに記録された*観察*を反映し、*洞察*を形成および更新します。
+ その後、メモリの再統合を実行して、記憶片間の矛盾や重複が適切に処理されるようにします。
+
+⚙️ ベストプラクティス:
+
+- MemoryScopeは、長期記憶のコア機能に基づいて、長期記憶を持つ対話インターフェース(API)と長期記憶を持つコマンドライン対話の実践(CLI)を実装しています。
+- MemoryScopeは、現在人気のあるエージェントフレームワーク(AutoGen、AgentScope)を組み合わせて、ベストプラクティスを提供します。
+
+### 主な特徴
+
+⚡ 低い応答時間(RT):
+- システム内のバックエンド操作(メモリの統合、反映と再統合)は、フロントエンド操作(メモリの取得)と分離されています。
+- バックエンド操作は通常(および推奨される)キューに入れられるか、定期的に実行されるため、システムのユーザー応答時間(RT)はフロントエンド操作のみに依存し、約500ミリ秒です。
+
+🌲 階層的で一貫性のある記憶:
+- システムに保存される記憶片は階層構造になっており、*洞察*は同様のテーマの*観察*の集約から得られる高レベルの情報です。
+- 記憶片間の矛盾や重複は定期的に処理され、一貫性が保たれます。
+- ユーザーの虚偽の内容はフィルタリングされ、LLMの幻覚を避けることができます。
+
+⏰ 時間感覚:
+- メモリの取得とメモリの統合を実行する際に時間感覚があり、クエリが時間に言及している場合に正確な関連情報を取得できます。
+
+----
+
+## 💼 サポートされているモデルAPI
+
+| バックエンド | タスク | サポートされているモデルの一部 |
+|-------------------|------------|------------------------------------------------------------------------|
+| openai_backend | Generation | gpt-4o, gpt-4o-mini, gpt-4, gpt-3.5-turbo |
+| | Embedding | text-embedding-ada-002, text-embedding-3-large, text-embedding-3-small |
+| dashscope_backend | Generation | qwen-max, qwen-plus, qwen-plus, qwen2-72b-instruct |
+| | Embedding | text-embedding-v1, text-embedding-v2 |
+| | Reranker | gte-rerank |
+
+将来的には、より多くのモデルインターフェースとローカルデプロイメントのLLMおよび埋め込みサービスをサポートする予定です。
+
+## 🚀 インストール
+インストール方法については、[Installation.md](docs/installation.md)を参照してください。
+
+## 🍕 クイックスタート
+- [簡単な使用法(クイックスタート)](./examples/api/simple_usages.ipynb)
+- [AutoGenとの連携](./examples/api/autogen_example.md)
+- [MemoryScopeチャットボットとのCLI](./examples/cli/README.md)
+- [高度なカスタマイズ](./examples/advance/custom_operator.md)
+
+## 💡 貢献
+
+貢献は常に奨励されています!
+
+プルリクエストをコミットする前に、このリポジトリにpre-commitフックをインストールすることを強くお勧めします。
+これらのフックは、gitコミットを行うたびに実行される小さなハウスキーピングスクリプトであり、フォーマットとリンティングを自動的に処理します。
+```shell
+pip install -e .
+pre-commit install
+```
+
+詳細については、[貢献ガイド](./docs/contribution.md)を参照してください。
+
+## 📖 引用
+
+MemoryScopeを論文で使用する場合は、以下の引用を追加してください:
+
+```
+@software{MemoryScope,
+author = {Li Yu and
+ Tiancheng Qin and
+ Qingxu Fu and
+ Sen Huang and
+ Xianzhe Xu and
+ Zhaoyang Liu and
+ Boyin Liu},
+month = {09},
+title = {{MemoryScope}},
+url = {https://github.com/modelscope/MemoryScope},
+year = {2024}
+}
+```
diff --git a/memoryscope/README_ZH.md b/memoryscope/README_ZH.md
new file mode 100644
index 00000000..7411381a
--- /dev/null
+++ b/memoryscope/README_ZH.md
@@ -0,0 +1,124 @@
+[**English**](./README.md) | 中文 | [**日本語**](./README_JP.md)
+
+# MemoryScope
+
+
+
+为您的大语言模型聊天机器人配备强大且灵活的长期记忆系统。
+
+[](https://pypi.org/project/memoryscope/)
+[](https://pypi.org/project/memoryscope/)
+[](./LICENSE)
+[](https://modelscope.github.io/MemoryScope/zh/index.html#id1)
+[](https://modelscope.github.io/MemoryScope/zh/docs/api.html)
+[](https://modelscope.github.io/MemoryScope/zh/docs/contribution.html)
+
+
+
+
+----
+## 📰 新闻
+
+- **[2024-09-10]** 我们现在发布了 MemoryScope v0.1.1.0,该版本也可以在 [PyPI](https://pypi.org/simple/memoryscope/) 上获取!
+----
+
+## 🌟 什么是MemoryScope?
+MemoryScope可以为LLM聊天机器人提供强大且灵活的长期记忆能力,并提供了构建长期记忆能力的框架。
+MemoryScope可以用于个人助理、情感陪伴等记忆场景,通过长期记忆能力来不断学习,记得用户的基础信息以及各种习惯和喜好,使得用户在使用LLM时逐渐感受到一种“默契”。
+
+### Demo
+
+
+
+
+
+### 核心框架:
+
+
+
+
+💾 记忆数据库: MemoryScope配备了向量数据库(默认是*ElasticSearch*),用于存储系统中记录的所有记忆片段。
+
+🔧 核心worker库: MemoryScope将长期记忆的能力原子化,抽象成单独的worker,包括query信息过滤,observation抽取,insight更新等20+worker。
+
+🛠️ 核心Operation库: MemoryScope将workers组织为工作流(workflow),构建了处理记忆的完善Operation库,实现了记忆检索,记忆巩固等核心能力。
+
+- 记忆检索:当用户输入对话,此操作返回语义相关的记忆片段。如果输入对话包含对时间的指涉,则同时返回相应时间中的记忆片段。
+- 记忆巩固:此操作接收一批用户的输入对话,并从对话中提取重要的用户信息,将其作为 *observation* 形式的记忆片段存储在记忆数据库中。
+- 反思与再巩固:每隔一段时间,此操作对新记录的 *observations* 进行反思,以形成和更新 *insight* 形式的记忆片段。然后执行记忆再巩固,以确保记忆片段之间的矛盾和重复得到妥善处理。
+
+⚙️ 最佳实践:
+
+- MemoryScope在构建了长期记忆核心能力的基础上,实现了带长期记忆的对话接口(API)和带长期记忆的命令行对话实践(CLI)。
+- MemoryScope结合了目前流行的Agent框架(AutoGen、AgentScope),给出了最佳实践。
+
+### 🤝主要特点
+
+⚡ 极低的线上时延(RT):
+- 系统中后端操作(记忆巩固、反思和再巩固)与前端操作(记忆检索)相互独立。
+- 由于后端操作通常(并且推荐)通过队列或每隔固定间隔执行,系统的用户时延(RT)完全取决于前端操作,仅为约500毫秒。
+
+🌲 记忆存储的层次结构和内容的连贯一致性:
+- 系统中存储的记忆片段采用分层结构,通过汇总主题相似的 *observations* 生成高层次的 *insights* 信息。
+- 定期处理记忆片段之间的矛盾和重复,以保证记忆内容的连贯一致性。
+- 过滤掉用户输入的虚构内容,以避免LLM产生幻觉。
+
+⏰ 时间敏感性:
+- 在执行记忆检索和记忆巩固时具备时间敏感性,因此在输入对话包含对时间的指涉时,可以检索到准确的相关信息。
+
+----
+
+## 💼 支持的模型API
+
+| Backend | Task | Some Supported Models |
+|-------------------|------------|------------------------------------------------------------------------|
+| openai_backend | Generation | gpt-4o, gpt-4o-mini, gpt-4, gpt-3.5-turbo |
+| | Embedding | text-embedding-ada-002, text-embedding-3-large, text-embedding-3-small |
+| dashscope_backend | Generation | qwen-max, qwen-plus, qwen-plus, qwen2-72b-instruct |
+| | Embedding | text-embedding-v1, text-embedding-v2 |
+| | Reranker | gte-rerank |
+
+未来将支持更多的模型接口和支持本地部署的LLM和emb服务
+
+----
+## 🚀 安装
+完整的安装方法请参考[安装指南](docs/installation_zh.md)。
+
+## 🍕 快速开始
+- [简易用法(快速开始)](./examples/api/simple_usages_zh.ipynb)
+- [在命令行与MemoryScope聊天机器人交互](./examples/cli/CLI_README_ZH.md)
+- [进阶自定义用法](./examples/advance/custom_operator.md)
+- [结合AutoGen使用](./examples/api/autogen_example.md)
+
+
+## 💡 代码贡献
+欢迎社区的代码贡献。
+我们非常推荐每一个贡献者在代码提交前,安装`pre-commit`钩子工具,
+能够帮助在每一次git提交的时候,进行自动化的代码格式校验。
+```shell
+pip install -e .
+pre-commit install
+```
+请参阅我们的[贡献指南](./docs/contribution_zh.md) 了解更多详细信息。
+
+## 📖 引用
+
+如果您在论文中有使用该项目,请添加以下引用:
+
+```
+@software{MemoryScope,
+author = {Li Yu and
+ Tiancheng Qin and
+ Qingxu Fu and
+ Sen Huang and
+ Xianzhe Xu and
+ Zhaoyang Liu and
+ Boyin Liu},
+month = {09},
+title = {{MemoryScope}},
+url = {https://github.com/modelscope/MemoryScope},
+year = {2024}
+}
+```
diff --git a/memoryscope/clear-vector-store.py b/memoryscope/clear-vector-store.py
new file mode 100644
index 00000000..c575d34a
--- /dev/null
+++ b/memoryscope/clear-vector-store.py
@@ -0,0 +1,24 @@
+"""
+Warning!
+
+This script purges the entire vector store !
+
+"""
+
+from memoryscope import MemoryScope, Arguments
+
+arguments = Arguments(
+ language="en",
+ human_name="user",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="openai_generation",
+ generation_model="gpt-4o",
+ embedding_backend="openai_embedding",
+ embedding_model="text-embedding-3-small",
+ enable_ranker=False,
+)
+
+ms = MemoryScope(arguments=arguments)
+es_store = ms.context.memory_store.es_store
+es_store.sync_delete_all()
diff --git a/memoryscope/docker-compose.yml b/memoryscope/docker-compose.yml
new file mode 100644
index 00000000..76483656
--- /dev/null
+++ b/memoryscope/docker-compose.yml
@@ -0,0 +1,16 @@
+services:
+ memory_scope_main:
+ image: ghcr.io/modelscope/memoryscope_arm:main
+# image: ghcr.io/modelscope/memoryscope_arm:main # For ARM architecture
+ environment:
+ DASHSCOPE_API_KEY: "sk-0000000000"
+# OPENAI_API_KEY: "sk-0000000000"
+ volumes:
+ - ./memoryscope/core/config:/memory_scope_project/memoryscope/memoryscope/core/config
+ deploy:
+ resources:
+ limits:
+ memory: 4G
+ stdin_open: true
+ tty: true
+# Please execute `docker compose run memory_scope_main` instead of `docker compose up`
diff --git a/memoryscope/docs/README.md b/memoryscope/docs/README.md
new file mode 100644
index 00000000..31d4286e
--- /dev/null
+++ b/memoryscope/docs/README.md
@@ -0,0 +1,19 @@
+# MemoryScope Documentation
+
+## Build Documentation
+
+Please use the following commands to build sphinx doc of MemoryScope.
+
+```shell
+# step 1: Install dependencies
+pip install sphinx sphinx-autobuild sphinx_rtd_theme myst-parser sphinxcontrib-mermaid
+
+# step 2: go into the sphinx_doc dir
+cd docs/sphinx_doc
+
+# step 3: build the sphinx doc
+./build_sphinx_doc.sh
+
+# step 4: view sphinx_doc/build/html/index.html using your browser
+cd docs/sphinx_doc/build/html && python -m http.server 8899
+```
diff --git a/memoryscope/docs/contribution.md b/memoryscope/docs/contribution.md
new file mode 100644
index 00000000..5775a81a
--- /dev/null
+++ b/memoryscope/docs/contribution.md
@@ -0,0 +1,42 @@
+# Contribute to MemoryScope
+Our community thrives on the diverse ideas and contributions of its members. Whether you're fixing a bug, adding a new feature, improving the documentation, or adding examples, your help is welcome. Here's how you can contribute:
+## Report Bugs and Ask For New Features?
+Did you find a bug or have a feature request? Please first check the issue tracker to see if it has already been reported. If not, feel free to open a new issue. Include as much detail as possible:
+- A descriptive title
+- Clear description of the issue
+- Steps to reproduce the problem
+- Version of the MemoryScope you are using
+- Any relevant code snippets or error messages
+## Contribute to Codebase
+### Fork and Clone the Repository
+To work on an issue or a new feature, start by forking the MemoryScope repository and then cloning your fork locally.
+```bash
+git clone https://github.com/your-username/memoryscope.git
+cd memoryscope
+```
+### Create a New Branch
+Create a new branch for your work. This helps keep proposed changes organized and separate from the `main` branch.
+```bash
+git checkout -b your-feature-branch-name
+```
+### Making Changes
+With your new branch checked out, you can now make your changes to the code. Remember to keep your changes as focused as possible. If you're addressing multiple issues or features, it's better to create separate branches and pull requests for each.
+We provide a developer version with additional `pre-commit` hooks to perform format checks compared to the official version:
+```bash
+# Install the developer version
+pip install -e .
+# Install pre-commit hooks
+pre-commit install
+```
+### Commit Your Changes
+Once you've made your changes, it's time to commit them. Write clear and concise commit messages that explain your changes.
+```bash
+git add -A
+git commit -m "A brief description of the changes"
+```
+You might get some error messages raised by `pre-commit`. Please resolve them according to the error code and commit again.
+### Submit a Pull Request
+When you're ready for feedback, submit a pull request to the MemoryScope `main` branch. In your pull request description, explain the changes you've made and any other relevant context.
+We will review your pull request. This process might involve some discussion, additional changes on your part, or both.
+### Code Review
+Wait for us to review your pull request. We may suggest some changes or improvements. Keep an eye on your GitHub notifications and be responsive to any feedback.
diff --git a/memoryscope/docs/contribution_zh.md b/memoryscope/docs/contribution_zh.md
new file mode 100644
index 00000000..5ed31e5d
--- /dev/null
+++ b/memoryscope/docs/contribution_zh.md
@@ -0,0 +1,51 @@
+# 贡献到MemoryScope
+我们的社区因其成员的多样化思想和贡献而兴旺发展。无论是修复一个错误,添加一个新功能,改进文档,还是添加示例,我们都欢迎您的帮助。以下是您做出贡献的方法:
+## 报告错误和提出新功能
+当您发现一个错误或者有一个功能请求,请首先检查问题跟踪器,查看它是否已经被报告。如果没有,随时可以开设一个新的问题。请包含尽可能多的细节:
+- 简明扼要的标题
+- 清晰地描述问题
+- 提供重现问题的步骤
+- 提供所使用的MemoryScope版本
+- 提供所有相关代码片段或错误信息
+## 对代码库做出贡献
+### Fork和Clone仓库
+要处理一个问题或新功能,首先要Fork仓库,然后将你的Fork克隆到本地。
+```bash
+git clone git@github.com:modelscope/MemoryScope.git
+cd MemoryScope
+```
+### 创建一个新分支
+为您的工作创建一个新分支。这有助于保持拟议更改的组织性,并与`main`分支分离。
+```bash
+git checkout -b your-feature-branch-name
+```
+### 做出修改
+我们非常推荐每一个贡献者在代码提交前,安装`pre-commit`钩子工具,
+能够帮助在每一次git提交的时候,进行自动化的代码格式校验。
+```bash
+# 安装开发者版本
+pip install -e .
+# 安装 pre-commit 钩子
+pre-commit install
+```
+
+### 提交您的修改
+
+修改完成之后就是提交它们的时候了。请提供清晰而简洁的提交信息,以解释您的修改内容。
+
+```bash
+git add -A
+git commit -m "修改内容的简要描述"
+```
+
+运行时您可能会收到 `pre-commit` 给出的错误信息。请根据错误信息修改您的代码然后再次提交。
+
+### 提交 Pull Request
+
+当您准备好您的修改分支后,向MemoryScope的 `main` 分支提交一个Pull Request。在您的Pull Request描述中,解释您所做的修改以及其他相关的信息。
+
+我们将审查您的Pull Request。这个过程可能涉及一些讨论以及额外的代码修改。
+
+### 代码审查
+
+等待我们审核您的Pull Request。我们可能会提供一些更改或改进建议。请留意您的GitHub通知,并对反馈做出响应。
\ No newline at end of file
diff --git a/memoryscope/docs/images/framework.png b/memoryscope/docs/images/framework.png
new file mode 100644
index 00000000..53370a3e
Binary files /dev/null and b/memoryscope/docs/images/framework.png differ
diff --git a/memoryscope/docs/images/logo.png b/memoryscope/docs/images/logo.png
new file mode 100644
index 00000000..713d1ef0
Binary files /dev/null and b/memoryscope/docs/images/logo.png differ
diff --git a/memoryscope/docs/installation.md b/memoryscope/docs/installation.md
new file mode 100644
index 00000000..2d29d479
--- /dev/null
+++ b/memoryscope/docs/installation.md
@@ -0,0 +1,126 @@
+# Installing MemoryScope
+
+## I. Install with docker [Recommended] [x86_64]
+
+1. Clone the repository and edit settings
+ ```bash
+ # clone project
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # edit configuration, e.g. add api keys
+ vim memoryscope/core/config/demo_config.yaml
+ ```
+
+2. Build Docker image
+ ```bash
+ sudo docker build --network=host -t memoryscope .
+ ```
+ If you are using arm-based computers, modify command above into: `sudo docker build -f DockerfileArm --network=host -t memoryscope .`
+
+3. Launch Docker container
+ ```bash
+ sudo docker run -it --rm --net=host memoryscope
+ ```
+
+> [!Important]
+> To inspect memory shift during the conversation, modify command in step 3 to `sudo docker run -it --name=memoryscope_container --rm --net=host memoryscope`;
+> Then start a new terminal window and execute `sudo docker exec -it memoryscope_container python quick-start-demo.py --config_path=memoryscope/core/config/demo_config_zh.yaml`;
+> In the second window, input `/list_memory refresh_time=5` to inspect memory
+
+## II. Install with docker compose [Recommended] [x86_64]
+
+1. Clone the repository and edit settings
+ ```bash
+ # clone project
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # edit configuration, e.g. add api keys
+ vim memoryscope/core/config/demo_config.yaml
+ ```
+
+2. Edit `docker-compose.yml` to change environment variable.
+ ```
+ OPENAI_API_KEY: "sk-0000000000"
+ ```
+
+3. Run `docker-compose run memory_scope_main` to build and launch the memory-scope cli interface. (For ARM architecture, you should edit `docker-compose.yml`, changing `image: ghcr.io/modelscope/memoryscope:main` to `image: ghcr.io/modelscope/memoryscope_arm:main`)
+
+
+## III. Install from PyPI
+
+1. Install from PyPI
+ ```bash
+ pip install memoryscope
+ ```
+
+2. Run Elasticsearch service, refer to [elasticsearch documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html).
+The docker method is recommended:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+3. Test Chinese / Dashscope Configuration
+ ```bash
+ export DASHSCOPE_API_KEY="sk-0000000000"
+ memoryscope --language="cn" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="用户" \
+ --assistant_name="AI" \
+ --generation_backend="dashscope_generation" \
+ --generation_model="qwen-max" \
+ --embedding_backend="dashscope_embedding" \
+ --embedding_model="text-embedding-v2" \
+ --enable_ranker=True \
+ --rank_backend="dashscope_rank" \
+ --rank_model="gte-rerank"
+ ```
+
+4. Test English / OpenAI Configuration
+ ```bash
+ export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ memoryscope --language="en" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="User" \
+ --assistant_name="AI" \
+ --generation_backend="openai_generation" \
+ --generation_model="gpt-4o" \
+ --embedding_backend="openai_embedding" \
+ --embedding_model="text-embedding-3-small" \
+ --enable_ranker=False
+ ```
+
+## IV. Install from source
+
+1. Clone the repository and edit settings
+ ```bash
+ # clone project
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # edit configuration, e.g. add api keys
+ vim memoryscope/core/config/demo_config.yaml
+ ```
+
+2. Install
+ ```bash
+ pip install -e .
+ ```
+
+3. Run Elasticsearch service, refer to [elasticsearch documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html).
+The docker method is recommended:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+4. Launch memoryscope, also refer to [cli documents](../examples/cli/CLI_README.md)
+ ```bash
+ export OPENAI_API_KEY="sk-0000000000"
+ python quick-start-demo.py --config_path=memoryscope/core/config/demo_config_zh.yaml
+ ```
diff --git a/memoryscope/docs/installation_zh.md b/memoryscope/docs/installation_zh.md
new file mode 100644
index 00000000..656351a1
--- /dev/null
+++ b/memoryscope/docs/installation_zh.md
@@ -0,0 +1,129 @@
+# MemoryScope 安装指南
+
+## 一、使用 Docker 安装 [推荐]
+
+1. 克隆仓库并编辑配置
+ ```bash
+ # 克隆项目
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # 编辑配置,例如添加 API 密钥
+ vim memoryscope/core/config/demo_config_zh.yaml
+ ```
+
+2. 构建 Docker 镜像
+ ```bash
+ sudo docker build --network=host -t memoryscope .
+ ```
+ 备注:如果是arm架构的电脑,则必须使用另一个命令:`sudo docker build -f DockerfileArm --network=host -t memoryscope .`
+
+3. 启动 Docker 容器
+ ```bash
+ sudo docker run -it --rm --net=host memoryscope
+ ```
+
+
+> [!Important]
+> 如果需要观察Memory的变化请调整第3步的运行命令。首先执行 `sudo docker run -it --name=memoryscope_container --rm --net=host memoryscope`启动memoryscope;
+> 然后新建命令行窗口,运行`sudo docker exec -it memoryscope_container python quick-start-demo.py --config_path=memoryscope/core/config/demo_config_zh.yaml`;
+> 在第二个窗口,继续输入`/list_memory refresh_time=5`来检查实时的memory
+
+## 二、使用 Docker Compose 安装 [推荐] [x86_64]
+
+1. 克隆仓库并编辑配置
+ ```bash
+ # 克隆项目
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # 编辑配置,例如添加 API 密钥
+ vim memoryscope/core/config/demo_config_zh.yaml
+ ```
+
+2. 编辑 `docker-compose.yml` 文件以更改环境变量。
+ ```
+ DASHSCOPE_API_KEY: "sk-0000000000"
+ ```
+
+3. 运行 `docker-compose run memory_scope_main` 命令来构建并启动 MemoryScope CLI 界面。(备注:如果是arm架构,还需要手动将docker-compose.yml中的`ghcr.io/modelscope/memoryscope:main`修改成`ghcr.io/modelscope/memoryscope_arm:main`)
+
+
+## 三、通过 PYPI 安装
+
+1. 从 PyPI 安装:
+ ```bash
+ pip install memoryscope
+ ```
+
+2. 运行 Elasticsearch 服务,参照 [Elasticsearch 文档](https://www.elastic.co/guide/cn/elasticsearch/reference/current/getting-started.html)。
+推荐使用 Docker 方法:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+3. 测试中文 / Dashscope 对话配置:
+ ```bash
+ export DASHSCOPE_API_KEY="sk-0000000000"
+ memoryscope --language="cn" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="用户" \
+ --assistant_name="AI" \
+ --generation_backend="dashscope_generation" \
+ --generation_model="qwen-max" \
+ --embedding_backend="dashscope_embedding" \
+ --embedding_model="text-embedding-v2" \
+ --enable_ranker=True \
+ --rank_backend="dashscope_rank" \
+ --rank_model="gte-rerank"
+ ```
+
+4. 测试英文 / OpenAI 对话配置:
+ ```bash
+ export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ memoryscope --language="en" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="User" \
+ --assistant_name="AI" \
+ --generation_backend="openai_generation" \
+ --generation_model="gpt-4o" \
+ --embedding_backend="openai_embedding" \
+ --embedding_model="text-embedding-3-small" \
+ --enable_ranker=False
+ ```
+
+
+## 四、从源码安装
+
+1. 克隆仓库并编辑设置
+ ```bash
+ # 克隆项目
+ git clone https://github.com/modelscope/memoryscope
+ cd memoryscope
+ # 编辑配置,例如添加 API 密钥
+ vim memoryscope/core/config/demo_config_zh.yaml
+ ```
+
+2. 安装依赖
+ ```bash
+ pip install -e .
+ ```
+
+3. 运行 Elasticsearch 服务,参照 [Elasticsearch 文档](https://www.elastic.co/guide/cn/elasticsearch/reference/current/getting-started.html)。
+推荐使用 Docker 方法:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+4. 启动 MemoryScope,同时参考 [CLI 文档](../examples/cli/CLI_README_ZH.md)
+ ```bash
+ export DASHSCOPE_API_KEY="sk-0000000000"
+ python quick-start-demo.py --config_path=memoryscope/core/config/demo_config_zh.yaml
+ ```
+
diff --git a/memoryscope/docs/sphinx_doc/Makefile b/memoryscope/docs/sphinx_doc/Makefile
new file mode 100644
index 00000000..dc1c479b
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/Makefile
@@ -0,0 +1,32 @@
+# Makefile
+
+SPHINXBUILD = sphinx-build
+SPHINXPROJ = MemoryScope-Doc
+ASSETSDIR = assets
+BUILDDIR = build/html
+SOURCEDIR_EN = en/source
+BUILDDIR_EN = build/html/en
+SOURCEDIR_ZH = zh/source
+BUILDDIR_ZH = build/html/zh
+
+# English document
+en:
+ @$(SPHINXBUILD) -b html "$(SOURCEDIR_EN)" "$(BUILDDIR_EN)"
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR_EN)"
+
+# Chinese document
+zh:
+ @$(SPHINXBUILD) -b html "$(SOURCEDIR_ZH)" "$(BUILDDIR_ZH)"
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR_ZH)"
+
+index:
+ @cp "$(ASSETSDIR)/redirect.html" "$(BUILDDIR)/index.html"
+
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR_EN)" "$(BUILDDIR_EN)" $(O)
+
+all: en zh index
+
+.PHONY: all en zh index
\ No newline at end of file
diff --git a/memoryscope/docs/sphinx_doc/assets/redirect.html b/memoryscope/docs/sphinx_doc/assets/redirect.html
new file mode 100644
index 00000000..81065c59
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/assets/redirect.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ MemoryScope Documentation
+
+
+ Redirecting to English documentation...
+ If you are not redirected, click here .
+
+
diff --git a/memoryscope/docs/sphinx_doc/build_sphinx_doc.sh b/memoryscope/docs/sphinx_doc/build_sphinx_doc.sh
new file mode 100644
index 00000000..cac49c49
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/build_sphinx_doc.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# remove build
+rm -rf build/html/*
+rm -rf en/source/memoryscope*.rst
+rm -rf zh/source/memoryscope*.rst
+rm -rf ja/source/memoryscope*.rst
+
+# copy related files
+cd ../../
+
+cp README.md docs/sphinx_doc/en/source/README.md
+cp docs/installation.md docs/sphinx_doc/en/source/docs/installation.md
+cp docs/contribution.md docs/sphinx_doc/en/source/docs/contribution.md
+cp -r docs/images docs/sphinx_doc/en/source/docs/images
+cp -r examples docs/sphinx_doc/en/source/examples
+
+cp README_ZH.md docs/sphinx_doc/zh/source/README.md
+cp docs/installation_zh.md docs/sphinx_doc/zh/source/docs/installation.md
+cp docs/contribution_zh.md docs/sphinx_doc/zh/source/docs/contribution.md
+cp -r docs/images docs/sphinx_doc/zh/source/docs/images
+cp -r examples docs/sphinx_doc/zh/source/examples
+
+cp README_JP.md docs/sphinx_doc/ja/source/README.md
+cp docs/installation_jp.md docs/sphinx_doc/ja/source/docs/installation.md
+cp docs/contribution_jp.md docs/sphinx_doc/ja/source/docs/contribution.md
+cp -r docs/images docs/sphinx_doc/ja/source/docs/images
+cp -r examples docs/sphinx_doc/ja/source/examples
+
+# build
+cd docs/sphinx_doc
+sphinx-apidoc -f -o en/source ../../memoryscope -t template -e
+sphinx-apidoc -f -o zh/source ../../memoryscope -t template -e
+sphinx-apidoc -f -o ja/source ../../memoryscope -t template -e
+
+# clear redundant files
+make clean all
+
+rm en/source/README.md
+rm en/source/docs/installation.md
+rm en/source/docs/contribution.md
+rm -rf en/source/docs/images
+rm -rf en/source/examples
+
+rm zh/source/README.md
+rm zh/source/docs/installation.md
+rm zh/source/docs/contribution.md
+rm -rf zh/source/docs/images
+rm -rf zh/source/examples
+
+rm ja/source/README.md
+rm ja/source/docs/installation.md
+rm ja/source/docs/contribution.md
+rm -rf ja/source/docs/images
+rm -rf ja/source/examples
diff --git a/memoryscope/docs/sphinx_doc/en/source/_static/custom.css b/memoryscope/docs/sphinx_doc/en/source/_static/custom.css
new file mode 100644
index 00000000..68f11cee
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/_static/custom.css
@@ -0,0 +1,4 @@
+.language-selector a {
+ color: white;
+ width: 20px;
+}
\ No newline at end of file
diff --git a/memoryscope/docs/sphinx_doc/en/source/_templates/language_selector.html b/memoryscope/docs/sphinx_doc/en/source/_templates/language_selector.html
new file mode 100644
index 00000000..86fe0703
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/_templates/language_selector.html
@@ -0,0 +1,5 @@
+
+
diff --git a/memoryscope/docs/sphinx_doc/en/source/_templates/layout.html b/memoryscope/docs/sphinx_doc/en/source/_templates/layout.html
new file mode 100644
index 00000000..1d182d30
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/_templates/layout.html
@@ -0,0 +1,3 @@
+
+{% extends "!layout.html" %} {% block sidebartitle %} {{ super() }} {% include
+"language_selector.html" %} {% endblock %}
diff --git a/memoryscope/docs/sphinx_doc/en/source/conf.py b/memoryscope/docs/sphinx_doc/en/source/conf.py
new file mode 100644
index 00000000..ca707ba1
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/conf.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath("../../../../../MemoryScope"))
+
+
+# -- Project information -----------------------------------------------------
+
+language = "en"
+
+project = "MemoryScope"
+copyright = "2024, Alibaba Tongyi Lab"
+author = "EcoML team of Alibaba Tongyi Lab"
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.napoleon",
+ "sphinxcontrib.mermaid",
+ "myst_parser",
+ "sphinx.ext.autosectionlabel",
+ "sphinxcontrib.autodoc_pydantic",
+ "nbsphinx"
+]
+
+autodoc_pydantic_model_show_json = True
+autodoc_pydantic_settings_show_json = True
+
+# Prefix document path to section labels, otherwise autogenerated labels would
+# look like 'heading' rather than 'path/to/file:heading'
+autosectionlabel_prefix_document = True
+autosummary_generate = True
+autosummary_ignore_module_all = False
+
+autodoc_member_order = "bysource"
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = False
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+autodoc_default_flags = ["members"]
+
+autodoc_default_options = {
+ "members": True,
+ "member-order": "bysource",
+ "special-members": "__init__",
+}
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# html_logo = "_static/logo.png"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+html_theme_options = {
+ # "logo_only": True,
+ "navigation_depth": 4,
+}
+
+source_suffix = {
+ ".rst": "restructuredtext",
+ ".md": "markdown",
+}
+
+html_css_files = [
+ "custom.css",
+]
diff --git a/memoryscope/docs/sphinx_doc/en/source/docs/api.rst b/memoryscope/docs/sphinx_doc/en/source/docs/api.rst
new file mode 100644
index 00000000..e7ddd17f
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/docs/api.rst
@@ -0,0 +1,68 @@
+.. _api:
+
+
+MemoryScope API Documentation
+
+
+Enumeration
+===========
+
+.. automodule:: memoryscope.enumeration
+ :members:
+
+Scheme
+======
+.. automodule:: memoryscope.scheme
+ :members:
+
+Config
+======
+.. automodule:: memoryscope.core.config
+ :members:
+
+
+Models
+======
+.. automodule:: memoryscope.core.models
+ :members:
+
+
+
+Storage
+=======
+.. automodule:: memoryscope.core.storage
+ :members:
+
+
+Worker
+======
+Base
+----
+
+.. automodule:: memoryscope.core.worker
+ :members:
+
+Frontend
+--------
+.. automodule:: memoryscope.core.worker.frontend
+ :members:
+
+Backend
+--------
+.. automodule:: memoryscope.core.worker.backend
+ :members:
+
+Operation
+=========
+.. automodule:: memoryscope.core.operation
+ :members:
+
+Service
+=======
+.. automodule:: memoryscope.core.service
+ :members:
+
+Chat
+====
+.. automodule:: memoryscope.core.chat
+ :members:
diff --git a/memoryscope/docs/sphinx_doc/en/source/index.rst b/memoryscope/docs/sphinx_doc/en/source/index.rst
new file mode 100644
index 00000000..825b4c4e
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/index.rst
@@ -0,0 +1,57 @@
+.. MemoryScope documentation master file, created by
+ sphinx-quickstart on Fri Jan 5 17:53:54 2024.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+:github_url: https://github.com/modelscope/memoryscope
+
+MemoryScope Documentation
+=========================
+
+Welcome to MemoryScope Tutorial
+-------------------------------
+
+.. image:: docs/images/logo.png
+ :align: center
+
+MemoryScope provides LLM chatbots with powerful and flexible long-term memory capabilities, offering a framework for building such abilities.
+It can be applied to scenarios like personal assistants and emotional companions, continuously learning through long-term memory to remember users' basic information as well as various habits and preferences.
+This allows users to gradually experience a sense of "understanding" when using the LLM.
+
+.. image:: docs/images/framework.png
+ :align: center
+
+Framework
+^^^^^^^^^^^^^^^^^^^^
+
+💾 Memory Database: MemoryScope is equipped with a vector database (default is *ElasticSearch*) to store all memory fragments recorded in the system.
+
+🔧 Worker Library: MemoryScope atomizes the capabilities of long-term memory into individual workers, including over 20 workers for tasks such as query information filtering, observation extraction, and insight updating.
+
+🛠️ Operation Library: Based on the worker pipeline, it constructs the operations for memory services, realizing key capabilities such as memory retrieval and memory consolidation.
+
+- Memory Retrieval: Upon arrival of a user query, this operation returns the semantically related memory pieces
+and/or those from the corresponding time if the query involves reference to time.
+- Memory Consolidation: This operation takes in a batch of user queries and returns important user information
+extracted from the queries as consolidated *observations* to be stored in the memory database.
+- Reflection and Re-consolidation: At regular intervals, this operation performs reflection upon newly recorded *observations*
+to form and update *insights*. Then, memory re-consolidation is performed to ensure contradictions and repetitions
+among memory pieces are properly handled.
+
+.. toctree::
+ :maxdepth: 2
+ :caption: MemoryScope Tutorial
+
+ About MemoryScope
+ Installation
+ Cli Client
+ Simple Usages
+ Advanced usage
+ Contribution
+
+
+.. toctree::
+ :maxdepth: 6
+ :caption: MemoryScope API Reference
+
+ API
diff --git a/memoryscope/docs/sphinx_doc/en/source/modules.rst b/memoryscope/docs/sphinx_doc/en/source/modules.rst
new file mode 100644
index 00000000..dd0343a8
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/en/source/modules.rst
@@ -0,0 +1,7 @@
+memoryscope
+===========
+
+.. toctree::
+ :maxdepth: 4
+
+ memoryscope
diff --git a/memoryscope/docs/sphinx_doc/ja/source/index.rst b/memoryscope/docs/sphinx_doc/ja/source/index.rst
new file mode 100644
index 00000000..50cee814
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/ja/source/index.rst
@@ -0,0 +1,55 @@
+.. MemoryScope documentation master file, created by
+ sphinx-quickstart on Fri Jan 5 17:53:54 2024.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+:github_url: https://github.com/modelscope/memoryscope
+
+MemoryScope ドキュメント
+=========================
+
+MemoryScopeに関するドキュメントへようこそ
+-------------------------------
+
+.. image:: ./docs/images/logo.png
+ :align: center
+
+MemoryScopeは、LLMチャットボットに強力で柔軟な長期記憶能力を提供し、長期記憶能力を構築するためのフレームワークを提供します。
+MemoryScopeは、個人アシスタントや感情的な伴侶などの記憶シナリオに使用でき、長期記憶能力を通じてユーザーの基本情報やさまざまな習慣や好みを覚え続けることができます。
+これにより、ユーザーはLLMを使用する際に徐々に「理解されている」感覚を体験することができます。
+
+.. image:: docs/images/framework.png
+ :align: center
+
+フレームワーク
+^^^^^^^^^^^^^^^^^^^^
+
+💾 メモリデータベース: MemoryScopeは、システム内に記録されたすべての記憶片を保存するためのベクトルデータベース(デフォルトは*ElasticSearch*)を備えています。
+
+🔧 ワーカーライブラリ: MemoryScopeは、長期記憶の能力を個々のワーカーに原子化し、クエリ情報のフィルタリング、観察の抽出、洞察の更新など、20以上のワーカーを含みます。
+
+🛠️ オペレーションライブラリ: ワーカーパイプラインに基づいて、メモリサービスのオペレーションを構築し、メモリの取得やメモリの統合などの主要な機能を実現します。
+
+- メモリの取得: ユーザークエリが到着すると、この操作は意味的に関連する記憶片を返します。
+ クエリが時間に言及している場合は、対応する時間の記憶片も返します。
+- メモリの統合: この操作は、一連のユーザークエリを受け取り、クエリから抽出された重要なユーザー情報を統合された*観察*としてメモリデータベースに保存します。
+- 反映と再統合: 定期的に、この操作は新たに記録された*観察*を反映し、*洞察*を形成および更新します。
+ その後、メモリの再統合を実行して、記憶片間の矛盾や重複が適切に処理されるようにします。
+
+.. toctree::
+ :maxdepth: 2
+ :caption: MemoryScope チュートリアル
+
+ MemoryScopeについて
+ インストール
+ CLIクライアント
+ 簡単な使用法
+ 高度な使用法
+ 貢献
+
+
+.. toctree::
+ :maxdepth: 6
+ :caption: MemoryScope APIリファレンス
+
+ API
diff --git a/memoryscope/docs/sphinx_doc/requirements.txt b/memoryscope/docs/sphinx_doc/requirements.txt
new file mode 100644
index 00000000..96833f9d
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/requirements.txt
@@ -0,0 +1,13 @@
+loguru
+tiktoken
+pillow
+requests
+openai
+numpy
+sphinx
+sphinx-autobuild
+sphinx_rtd_theme
+sphinxcontrib-mermaid
+myst-parser
+autodoc_pydantic
+nbsphinx
diff --git a/memoryscope/docs/sphinx_doc/zh/source/_static/custom.css b/memoryscope/docs/sphinx_doc/zh/source/_static/custom.css
new file mode 100644
index 00000000..68f11cee
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/_static/custom.css
@@ -0,0 +1,4 @@
+.language-selector a {
+ color: white;
+ width: 20px;
+}
\ No newline at end of file
diff --git a/memoryscope/docs/sphinx_doc/zh/source/_templates/language_selector.html b/memoryscope/docs/sphinx_doc/zh/source/_templates/language_selector.html
new file mode 100644
index 00000000..86fe0703
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/_templates/language_selector.html
@@ -0,0 +1,5 @@
+
+
diff --git a/memoryscope/docs/sphinx_doc/zh/source/_templates/layout.html b/memoryscope/docs/sphinx_doc/zh/source/_templates/layout.html
new file mode 100644
index 00000000..1d182d30
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/_templates/layout.html
@@ -0,0 +1,3 @@
+
+{% extends "!layout.html" %} {% block sidebartitle %} {{ super() }} {% include
+"language_selector.html" %} {% endblock %}
diff --git a/memoryscope/docs/sphinx_doc/zh/source/conf.py b/memoryscope/docs/sphinx_doc/zh/source/conf.py
new file mode 100644
index 00000000..b6cf426b
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/conf.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath("../../../../../MemoryScope"))
+
+
+# -- Project information -----------------------------------------------------
+
+language = "zh"
+
+project = "MemoryScope"
+copyright = "2024, Alibaba Tongyi Lab"
+author = "EcoML team of Alibaba Tongyi Lab"
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.napoleon",
+ "sphinxcontrib.mermaid",
+ "myst_parser",
+ "sphinx.ext.autosectionlabel",
+ "sphinxcontrib.autodoc_pydantic",
+ "nbsphinx"
+]
+
+autodoc_pydantic_model_show_json = True
+autodoc_pydantic_settings_show_json = True
+
+# Prefix document path to section labels, otherwise autogenerated labels would
+# look like 'heading' rather than 'path/to/file:heading'
+autosectionlabel_prefix_document = True
+autosummary_generate = True
+autosummary_ignore_module_all = False
+
+autodoc_member_order = "bysource"
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = False
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+autodoc_default_flags = ["members"]
+
+autodoc_default_options = {
+ "members": True,
+ "member-order": "bysource",
+ "special-members": "__init__",
+}
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# html_logo = "_static/logo.png"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+html_theme_options = {
+ # "logo_only": True,
+ "navigation_depth": 4,
+}
+
+source_suffix = {
+ ".rst": "restructuredtext",
+ ".md": "markdown",
+}
+
+html_css_files = [
+ "custom.css",
+]
diff --git a/memoryscope/docs/sphinx_doc/zh/source/docs/api.rst b/memoryscope/docs/sphinx_doc/zh/source/docs/api.rst
new file mode 100644
index 00000000..5a5ee06c
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/docs/api.rst
@@ -0,0 +1,68 @@
+.. _api:
+
+
+MemoryScope API 接口文档
+
+
+Enumeration
+===========
+
+.. automodule:: memoryscope.enumeration
+ :members:
+
+Scheme
+======
+.. automodule:: memoryscope.scheme
+ :members:
+
+Config
+======
+.. automodule:: memoryscope.core.config
+ :members:
+
+
+Models
+======
+.. automodule:: memoryscope.core.models
+ :members:
+
+
+
+Storage
+=======
+.. automodule:: memoryscope.core.storage
+ :members:
+
+
+Worker
+======
+Base
+----
+
+.. automodule:: memoryscope.core.worker
+ :members:
+
+Frontend
+--------
+.. automodule:: memoryscope.core.worker.frontend
+ :members:
+
+Backend
+--------
+.. automodule:: memoryscope.core.worker.backend
+ :members:
+
+Operation
+=========
+.. automodule:: memoryscope.core.operation
+ :members:
+
+Service
+=======
+.. automodule:: memoryscope.core.service
+ :members:
+
+Chat
+====
+.. automodule:: memoryscope.core.chat
+ :members:
diff --git a/memoryscope/docs/sphinx_doc/zh/source/index.rst b/memoryscope/docs/sphinx_doc/zh/source/index.rst
new file mode 100644
index 00000000..138f696f
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/index.rst
@@ -0,0 +1,53 @@
+.. MemoryScope documentation master file, created by
+ sphinx-quickstart on Fri Jan 5 17:53:54 2024.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+:github_url: https://github.com/modelscope/memoryscope
+
+MemoryScope 文档
+=========================
+
+欢迎浏览MemoryScope相关文档
+-------------------------------
+
+.. image:: ./docs/images/logo.png
+ :align: center
+
+MemoryScope可以为LLM聊天机器人提供强大且灵活的长期记忆能力,并提供了构建长期记忆能力的框架。
+MemoryScope可以用于个人助理、情感陪伴等记忆场景,通过长期记忆能力来不断学习,记得用户的基础信息以及各种习惯和喜好,使得用户在使用LLM时逐渐感受到一种“默契”。
+
+.. image:: docs/images/framework.png
+ :align: center
+
+核心框架
+^^^^^^^^^^^^^^^^^^^^
+
+💾 记忆数据库: MemoryScope配备了向量数据库(默认是*ElasticSearch*),用于存储系统中记录的所有记忆片段。
+
+🔧 核心worker库: MemoryScope将长期记忆的能力原子化,抽象成单独的worker,包括query信息过滤,observation抽取,insight更新等20+worker。
+
+🛠️ 核心Op库: 并基于worker的pipeline构建了memory服务的核心operation,实现了记忆检索,记忆巩固等核心能力。
+
+- 记忆检索:当用户输入对话,此操作返回语义相关的记忆片段。如果输入对话包含对时间的指涉,则同时返回相应时间中的记忆片段。
+- 记忆巩固:此操作接收一批用户的输入对话,并从对话中提取重要的用户信息,将其作为 *observation* 形式的记忆片段存储在记忆数据库中。
+- 反思与再巩固:每隔一段时间,此操作对新记录的 *observations* 进行反思,以形成和更新 *insight*
+ 形式的记忆片段。然后执行记忆再巩固,以确保记忆片段之间的矛盾和重复得到妥善处理。
+
+.. toctree::
+ :maxdepth: 2
+ :caption: MemoryScope 教程
+
+ 关于 MemoryScope
+ 安装
+ 命令行终端
+ 简单案例
+ 高级用法
+ 贡献
+
+.. toctree::
+ :maxdepth: 6
+ :caption: MemoryScope 接口
+
+ API
+
diff --git a/memoryscope/docs/sphinx_doc/zh/source/modules.rst b/memoryscope/docs/sphinx_doc/zh/source/modules.rst
new file mode 100644
index 00000000..dd0343a8
--- /dev/null
+++ b/memoryscope/docs/sphinx_doc/zh/source/modules.rst
@@ -0,0 +1,7 @@
+memoryscope
+===========
+
+.. toctree::
+ :maxdepth: 4
+
+ memoryscope
diff --git a/memoryscope/examples/advance/custom_operator.md b/memoryscope/examples/advance/custom_operator.md
new file mode 100644
index 00000000..654f55be
--- /dev/null
+++ b/memoryscope/examples/advance/custom_operator.md
@@ -0,0 +1,49 @@
+# Custom Operator and Worker
+
+1. Create a new worker named `example_query_worker.py` in the `contrib` directory:
+ ```bash
+ vim memoryscope/contrib/example_query_worker.py
+ ```
+
+2. Write the program for the new custom worker. Note that the class name must match the filename, which is `ExampleQueryWorker`:
+ ```python
+ import datetime
+ from memoryscope.constants.common_constants import QUERY_WITH_TS
+ from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+
+ class ExampleQueryWorker(MemoryBaseWorker):
+ def _run(self):
+ timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default
+ assert "query" in self.chat_kwargs
+ query = self.chat_kwargs["query"]
+ if not query:
+ query = ""
+ else:
+ query = query.strip() + "\n You must add a `meow~` at the end of each of your answers."
+ # Store the determined query and its timestamp in the context
+ self.set_workflow_context(QUERY_WITH_TS, (query, timestamp))
+ ```
+
+3. Create a YAML startup file (copying `demo_config.yaml`):
+ ```
+ cp memoryscope/core/config/demo_config.yaml examples/advance/replacement.yaml
+ vim examples/advance/replacement.yaml
+ ```
+
+4. At the bottom, insert the definition for the new worker and replace the previous default `set_query` worker, and update the operation's workflow:
+ ```
+ rewrite_query:
+ class: contrib.example_query_worker
+ generation_model: generation_model
+ ```
+ ```
+ retrieve_memory:
+ class: core.operation.frontend_operation
+ workflow: rewrite_query,[extract_time|retrieve_obs_ins,semantic_rank],fuse_rerank
+ description: "retrieve long-term memory"
+ ```
+
+5. Verify:
+ ```
+ python quick-start-demo.py --config examples/advance/replacement.yaml
+ ```
\ No newline at end of file
diff --git a/memoryscope/examples/advance/custom_operator_zh.md b/memoryscope/examples/advance/custom_operator_zh.md
new file mode 100644
index 00000000..48333968
--- /dev/null
+++ b/memoryscope/examples/advance/custom_operator_zh.md
@@ -0,0 +1,53 @@
+# 自定义 Operator 和 Worker
+
+1. 在 `contrib` 路径下创建新worker,命名为 `example_query_worker.py`:
+ ```bash
+ vim memoryscope/contrib/example_query_worker.py
+ ```
+
+2. 写入新的自定义worker的程序,注意`class`的命名需要与文件名保持一致,为`ExampleQueryWorker`:
+ ```python
+ import datetime
+ from memoryscope.constants.common_constants import QUERY_WITH_TS
+ from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+
+ class ExampleQueryWorker(MemoryBaseWorker):
+
+ def _run(self):
+
+ timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default
+
+ assert "query" in self.chat_kwargs
+ query = self.chat_kwargs["query"]
+ if not query:
+ query = ""
+ else:
+ query = query.strip() + "\n You must add a `meow~` at the end of each of your answer."
+
+ # Store the determined query and its timestamp in the context
+ self.set_workflow_context(QUERY_WITH_TS, (query, timestamp))
+ ```
+
+3. 创建yaml启动文件(复制demo_config_zh.yaml)
+ ```
+ cp memoryscope/core/config/demo_config_zh.yaml examples/advance/replacement.yaml
+ vim examples/advance/replacement.yaml
+ ```
+
+4. 在最下面插入新worker的定义,并且取代之前的默认`set_query`worker,并替换operation的workflow
+ ```
+ rewrite_query:
+ class: contrib.example_query_worker
+ generation_model: generation_model
+ ```
+ ```
+ retrieve_memory:
+ class: core.operation.frontend_operation
+ workflow: rewrite_query,[extract_time|retrieve_obs_ins,semantic_rank],fuse_rerank
+ description: "retrieve long-term memory"
+ ```
+
+5. 验证:
+ ```
+ python quick-start-demo.py --config examples/advance/replacement.yaml
+ ```
diff --git a/memoryscope/examples/advance/replacement.yaml b/memoryscope/examples/advance/replacement.yaml
new file mode 100644
index 00000000..4ef847f0
--- /dev/null
+++ b/memoryscope/examples/advance/replacement.yaml
@@ -0,0 +1,185 @@
+global:
+ language: en
+ thread_pool_max_workers: 5
+ logger_name: memoryscope
+ logger_name_time_suffix: "%Y%m%d_%H%M%S"
+ logger_to_screen: false
+ enable_ranker: false
+ enable_today_contra_repeat: true
+ enable_long_contra_repeat: false
+ output_memory_max_count: 20
+
+memory_chat:
+ cli_memory_chat:
+ class: core.chat.cli_memory_chat
+ memory_service: memoryscope_service
+ generation_model: generation_model
+ stream: true
+
+memory_service:
+ memoryscope_service:
+ class: core.service.memory_scope_service
+ human_name: user
+ assistant_name: AI
+ memory_operations:
+ read_message:
+ class: core.operation.frontend_operation
+ workflow: read_message
+ description: "read short memory"
+
+ retrieve_memory:
+ class: core.operation.frontend_operation
+ workflow: rewrite_query,[extract_time|retrieve_obs_ins,semantic_rank],fuse_rerank
+ description: "retrieve long-term memory"
+
+ list_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_top_memory,print_memory
+ description: "read all long-term memory of the user, use `refresh_time=5` to refresh screen every 5 seconds."
+
+ delete_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_memory
+ description: "delete a single long-term memory"
+
+ delete_all:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_all
+ description: "delete all long-term memory"
+
+ add_memory:
+ class: core.operation.frontend_operation
+ workflow: add_memory
+ description: "add a single observation"
+
+ consolidate_memory:
+ class: core.operation.consolidate_memory_op
+ workflow: info_filter,[get_observation|get_observation_with_time|load_today_memory],contra_repeat,store_memory
+ description: "summary user's observation memory, run backend."
+ interval_time: 1
+
+ reflect_and_reconsolidate:
+ class: core.operation.backend_operation
+ workflow: load_obs_and_insight,get_reflection_subject,update_insight,long_contra_repeat,store_memory
+ description: "summary user's insight memory, run backend."
+ interval_time: 15
+
+worker:
+ dummy:
+ class: core.worker.dummy_worker
+ generation_model: generation_model
+ embedding_model: embedding_model
+ rank_model: rank_model
+ read_message:
+ class: core.worker.frontend.read_message_worker
+ set_query:
+ class: core.worker.frontend.set_query_worker
+ rewrite_query:
+ class: contrib.example_query_worker
+ generation_model: generation_model
+ retrieve_obs_ins:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ extract_time:
+ class: core.worker.frontend.extract_time_worker
+ generation_model: generation_model
+ semantic_rank:
+ class: core.worker.frontend.semantic_rank_worker
+ rank_model: rank_model
+ fuse_rerank:
+ class: core.worker.frontend.fuse_rerank_worker
+ fuse_score_threshold: 0.01
+ fuse_ratio_dict:
+ conversation: 0.5
+ observation: 1
+ obs_customized: 1.2
+ insight: 2.0
+ fuse_time_ratio: 2.0
+ retrieve_top_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ retrieve_expired_top_k: 100
+ print_memory:
+ class: core.worker.frontend.print_memory_worker
+ retrieve_all_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 1000
+ retrieve_ins_top_k: 1000
+ retrieve_expired_top_k: 1000
+ delete_memory:
+ class: core.worker.backend.update_memory_worker
+ method: delete_memory
+ delete_all:
+ class: core.worker.backend.update_memory_worker
+ method: delete_all
+ add_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_query
+ info_filter:
+ class: core.worker.backend.info_filter_worker
+ generation_model: generation_model
+ load_today_memory:
+ class: core.worker.backend.load_memory_worker
+ retrieve_today_top_k: 100
+ get_observation:
+ class: core.worker.backend.get_observation_worker
+ generation_model: generation_model
+ get_observation_with_time:
+ class: core.worker.backend.get_observation_with_time_worker
+ generation_model: generation_model
+ contra_repeat:
+ class: core.worker.backend.contra_repeat_worker
+ generation_model: generation_model
+ store_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_memory_key
+ memory_key: all
+ load_obs_and_insight:
+ class: core.worker.backend.load_memory_worker
+ retrieve_not_reflected_top_k: 100
+ retrieve_not_updated_top_k: 100
+ retrieve_insight_top_k: 100
+ get_reflection_subject:
+ class: core.worker.backend.get_reflection_subject_worker
+ generation_model: generation_model
+ reflect_obs_cnt_threshold: 5
+ update_insight:
+ class: core.worker.backend.update_insight_worker
+ generation_model: generation_model
+ rank_model: rank_model
+ embedding_model: embedding_model
+ update_insight_threshold: 0.01
+ enable_parallel: false
+ long_contra_repeat:
+ class: core.worker.backend.long_contra_repeat_worker
+ generation_model: generation_model
+ long_contra_repeat_threshold: 0.5
+
+model:
+ generation_model:
+ class: core.models.llama_index_generation_model
+ module_name: dashscope_generation
+ model_name: qwen-max
+ max_tokens: 2000
+ temperature: 0.01
+ embedding_model:
+ class: core.models.llama_index_embedding_model
+ module_name: dashscope_embedding
+ model_name: text-embedding-v2
+ rank_model:
+ class: core.models.llama_index_rank_model
+ module_name: dashscope_rank
+ model_name: gte-rerank
+ top_n: 500
+
+memory_store:
+ class: core.storage.llama_index_es_memory_store
+ embedding_model: embedding_model
+ index_name: memory_index
+ es_url: http://localhost:9200
+ retrieve_mode: dense
+
+monitor:
+ class: core.storage.dummy_monitor
\ No newline at end of file
diff --git a/memoryscope/examples/api/agentscope_example.md b/memoryscope/examples/api/agentscope_example.md
new file mode 100644
index 00000000..28117a7b
--- /dev/null
+++ b/memoryscope/examples/api/agentscope_example.md
@@ -0,0 +1,22 @@
+# Working with AgentScope
+
+1. First, make sure that you have installed AutoGen as well as memoryscope.
+ ```
+ pip install agentscope memoryscope
+ ```
+
+
+2. Then, ensure that es is up and running. [elasticsearch documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html).
+The docker method is recommended:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+3. Finally, we can start the autogen demo.
+ ```
+ python examples/api/agentscope_example.py
+ ```
\ No newline at end of file
diff --git a/memoryscope/examples/api/agentscope_example.py b/memoryscope/examples/api/agentscope_example.py
new file mode 100644
index 00000000..2fb5be90
--- /dev/null
+++ b/memoryscope/examples/api/agentscope_example.py
@@ -0,0 +1,72 @@
+from typing import Optional, Union, Sequence
+
+import agentscope
+from agentscope.agents import AgentBase, UserAgent
+from agentscope.message import Msg
+
+from memoryscope import MemoryScope, Arguments
+
+
+class MemoryScopeAgent(AgentBase):
+ def __init__(self, name: str, arguments: Arguments, **kwargs) -> None:
+ # Disable AgentScope memory and use MemoryScope memory instead
+ super().__init__(name, use_memory=False, **kwargs)
+
+ # Create a memory client in MemoryScope
+ self.memory_scope = MemoryScope(arguments=arguments)
+ self.memory_chat = self.memory_scope.default_memory_chat
+
+ def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
+ # Generate response
+ response = self.memory_chat.chat_with_memory(query=x.content)
+
+ # Wrap the response in a message object in AgentScope
+ msg = Msg(name=self.name, content=response.message.content, role="assistant")
+
+ # Print/speak the message in this agent's voice
+ self.speak(msg)
+
+ return msg
+
+ def close(self):
+ # Close the backend service of MemoryScope
+ self.memory_scope.close()
+
+
+def main():
+ # Setting of MemoryScope
+ arguments = Arguments(
+ language="cn",
+ human_name="用户",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="dashscope_generation",
+ generation_model="qwen-max",
+ embedding_backend="dashscope_embedding",
+ embedding_model="text-embedding-v2",
+ rank_backend="dashscope_rank",
+ rank_model="gte-rerank")
+
+ # Initialize AgentScope
+ agentscope.init(project="MemoryScope")
+
+ memoryscope_agent = MemoryScopeAgent(name="Assistant", arguments=arguments)
+
+ user_agent = UserAgent()
+
+ # Dialog
+ msg = None
+ while True:
+ # User input
+ msg = user_agent(msg)
+ if msg.content == "exit":
+ break
+ # Agent speaks
+ msg = memoryscope_agent(msg)
+
+ # End memory
+ memoryscope_agent.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/memoryscope/examples/api/autogen_example.md b/memoryscope/examples/api/autogen_example.md
new file mode 100644
index 00000000..b6cecb20
--- /dev/null
+++ b/memoryscope/examples/api/autogen_example.md
@@ -0,0 +1,22 @@
+# Working with AutoGen
+
+1. First, make sure that you have installed AutoGen as well as memoryscope.
+ ```
+ pip install pyautogen memoryscope
+ ```
+
+
+2. Then, ensure that es is up and running. [elasticsearch documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html).
+The docker method is recommended:
+ ```
+ sudo docker run -p 9200:9200 \
+ -e "discovery.type=single-node" \
+ -e "xpack.security.enabled=false" \
+ -e "xpack.license.self_generated.type=trial" \
+ docker.elastic.co/elasticsearch/elasticsearch:8.13.2
+ ```
+
+3. Finally, we can start the autogen demo.
+ ```
+ python examples/api/autogen_example.py
+ ```
\ No newline at end of file
diff --git a/memoryscope/examples/api/autogen_example.py b/memoryscope/examples/api/autogen_example.py
new file mode 100644
index 00000000..a654e32a
--- /dev/null
+++ b/memoryscope/examples/api/autogen_example.py
@@ -0,0 +1,79 @@
+from typing import Optional, Union, Literal, Dict, List, Any, Tuple
+
+from autogen import Agent, ConversableAgent, UserProxyAgent
+
+from memoryscope import MemoryScope, Arguments
+
+
+class MemoryScopeAgent(ConversableAgent):
+ def __init__(
+ self,
+ name: str = "assistant",
+ system_message: Optional[str] = "",
+ human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "NEVER",
+ llm_config: Optional[Union[Dict, bool]] = None,
+ arguments: Arguments = None,
+ **kwargs,
+ ):
+ super().__init__(
+ name=name,
+ system_message=system_message,
+ human_input_mode=human_input_mode,
+ llm_config=llm_config,
+ **kwargs,
+ )
+
+ # Create a memory client in MemoryScope
+ self.memory_scope = MemoryScope(arguments=arguments)
+ self.memory_chat = self.memory_scope.default_memory_chat
+
+ self.register_reply([Agent, None], MemoryScopeAgent.generate_reply_with_memory, remove_other_reply_funcs=True)
+
+ def generate_reply_with_memory(
+ self,
+ messages: Optional[List[Dict]] = None,
+ sender: Optional[Agent] = None,
+ config: Optional[Any] = None,
+ ) -> Tuple[bool, Union[str, Dict, None]]:
+ # Generate response
+
+ contents = []
+ for message in messages:
+ if message.get("role") != self.name:
+ contents.append(message.get("content", ""))
+
+ query = contents[-1]
+ response = self.memory_chat.chat_with_memory(query=query)
+ return True, response.message.content
+
+ def close(self):
+ self.memory_scope.close()
+
+
+def main():
+ # Create the agent of MemoryScope
+ arguments = Arguments(
+ language="cn",
+ human_name="用户",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="dashscope_generation",
+ generation_model="qwen-max",
+ embedding_backend="dashscope_embedding",
+ embedding_model="text-embedding-v2",
+ rank_backend="dashscope_rank",
+ rank_model="gte-rerank"
+ )
+
+ assistant = MemoryScopeAgent("assistant", arguments=arguments)
+
+ # Create the agent that represents the user in the conversation.
+ user_proxy = UserProxyAgent("user", code_execution_config=False)
+
+ # Let the assistant start the conversation. It will end when the user types exit.
+ assistant.initiate_chat(user_proxy, message="有什么需要帮忙的吗?")
+ assistant.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/memoryscope/examples/api/chat_example.py b/memoryscope/examples/api/chat_example.py
new file mode 100644
index 00000000..06d668a2
--- /dev/null
+++ b/memoryscope/examples/api/chat_example.py
@@ -0,0 +1,105 @@
+from memoryscope import MemoryScope, Arguments
+
+arguments = Arguments(
+ language="cn",
+ human_name="用户",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="dashscope_generation",
+ generation_model="qwen-max",
+ embedding_backend="dashscope_embedding",
+ embedding_model="text-embedding-v2",
+ rank_backend="dashscope_rank",
+ rank_model="gte-rerank",
+ enable_ranker=True)
+
+
+def chat_example1():
+ with MemoryScope(config_path="memoryscope/core/config/demo_config_zh.yaml") as ms:
+ memory_chat = ms.default_memory_chat
+ response = memory_chat.chat_with_memory(query="我的爱好是弹琴。")
+ print("回答:\n" + response.message.content)
+ print("记忆:\n" + response.meta_data["memories"])
+
+
+def chat_example2():
+ with MemoryScope(arguments=arguments) as ms:
+ memory_chat = ms.default_memory_chat
+ response = memory_chat.chat_with_memory(query="我的爱好是弹琴。")
+ print("回答:\n" + response.message.content)
+ print("记忆:\n" + response.meta_data["memories"])
+
+
+def chat_example3():
+ with MemoryScope(arguments=arguments) as ms:
+ memory_chat = ms.default_memory_chat
+ response = memory_chat.chat_with_memory(
+ query="你知道我消灭了几个星系吗?",
+ temporary_memories="张三是一只哥斯拉,这是真是的,不是虚构的。\n前天张三消灭了星系0x4be?\n大前天张三消灭了星系0x5be?")
+ print("回答:\n" + response.message.content)
+
+
+def chat_example4():
+ with MemoryScope(arguments=arguments) as ms:
+ memory_chat = ms.default_memory_chat
+ memory_chat.run_service_operation("delete_all")
+
+ response = memory_chat.chat_with_memory(query="我的爱好是弹琴。")
+ print("回答1:\n" + response.message.content)
+ result = memory_chat.run_service_operation("consolidate_memory")
+ print(result)
+
+ response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?", history_message_strategy=None)
+ print("回答2:\n" + response.message.content)
+ print("记忆2:\n" + response.meta_data["memories"])
+
+
+def chat_example5():
+ with MemoryScope(arguments=arguments) as ms:
+ memory_service = ms.default_memory_service
+ memory_service.init_service()
+
+ result = memory_service.list_memory()
+ print(f"list_memory result={result}")
+
+ result = memory_service.retrieve_memory()
+ print(f"retrieve_memory result={result}")
+
+ result = memory_service.consolidate_memory()
+ print(f"consolidate_memory result={result}")
+
+
+def chat_example6():
+ with MemoryScope(arguments=arguments) as ms:
+ memory_chat = ms.default_memory_chat
+ memory_chat.run_service_operation("delete_all", "张三")
+ memory_chat.run_service_operation("delete_all", "李四")
+
+ print("李四=========================")
+ response = memory_chat.chat_with_memory(query="我的爱好是弹琴。", role_name="李四")
+ print("回答1:\n" + response.message.content)
+ result = memory_chat.run_service_operation("consolidate_memory", role_name="李四")
+ print(result)
+ response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?", role_name="李四",
+ history_message_strategy=None)
+ print("回答2:\n" + response.message.content)
+ print("记忆2:\n" + response.meta_data["memories"])
+
+ print("张三=========================")
+ response = memory_chat.chat_with_memory(query="我的爱好是打羽毛球。", role_name="张三")
+ print("回答1:\n" + response.message.content)
+ result = memory_chat.run_service_operation("consolidate_memory", role_name="张三")
+ print(result)
+ response = memory_chat.chat_with_memory(query="你知道我的运动爱好是什么?", role_name="张三",
+ history_message_strategy=None)
+ print("回答2:\n" + response.message.content)
+ print("记忆2:\n" + response.meta_data["memories"])
+
+
+if __name__ == "__main__":
+ # chat_example1()
+ # chat_example2()
+ # chat_example3()
+ chat_example4()
+ # chat_example5()
+ # chat_example6()
diff --git a/memoryscope/examples/api/simple_usages.ipynb b/memoryscope/examples/api/simple_usages.ipynb
new file mode 100644
index 00000000..4a9cde70
--- /dev/null
+++ b/memoryscope/examples/api/simple_usages.ipynb
@@ -0,0 +1,509 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Example usages of **chat** and **service** interfaces\n",
+ "This notebook shows simple usages of MemoryScope's **chat** and **service** interfaces, along with its main features.\n",
+ "\n",
+ "Before running this notebook, follow the [**Installation**](../../docs/installation.md#iii-install-from-pypi) guidelines in Readme, and start the Docker image first."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Initiate a MemoryScope instance\n",
+ "First, we need to specify a configuration and initiate a MemoryScope instance."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from memoryscope import MemoryScope, Arguments\n",
+ "arguments = Arguments(\n",
+ " language=\"en\",\n",
+ " human_name=\"User\",\n",
+ " assistant_name=\"AI\",\n",
+ " memory_chat_class=\"api_memory_chat\",\n",
+ " generation_backend=\"dashscope_generation\",\n",
+ " generation_model=\"qwen2-72b-instruct\",\n",
+ " embedding_backend=\"dashscope_embedding\",\n",
+ " embedding_model=\"text-embedding-v2\",\n",
+ " rank_backend=\"dashscope_rank\",\n",
+ " rank_model=\"gte-rerank\",\n",
+ " enable_ranker=True,\n",
+ " worker_params={\"get_reflection_subject\": {\"reflect_num_questions\": 3}}\n",
+ ")\n",
+ "\n",
+ "ms = MemoryScope(arguments=arguments)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Chat without memory\n",
+ "MemoryScope comes with a default **chat** interface, so it's very easy to start chatting, just as what you'll do with any LLM chatbot."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Response 1: \n",
+ "That's wonderful! Playing the piano is a beautiful and expressive hobby that can bring joy, relaxation, and a sense of achievement. It engages both the creative and technical aspects of your mind, enhancing cognitive skills and fostering emotional expression. Whether you enjoy classical pieces, modern compositions, or improvisation, the piano offers a vast repertoire to explore. Keep practicing and enjoying your musical journey!\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_chat = ms.default_memory_chat\n",
+ "memory_chat.run_service_operation(\"delete_all\")\n",
+ "response = memory_chat.chat_with_memory(query=\"My hobby is to play piano.\")\n",
+ "print(\"Response 1: \\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "You can choose to chat with or without multi-round conversation contexts. However, since **Memory Consolidation** has not been called, there's no memory pieces in the system yet."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Response 2: \n",
+ "Based on our conversation, you have mentioned that your hobby is to play the piano. Therefore, yes, you play a musical instrument – the piano.\n",
+ "Response 3: \n",
+ "I'm sorry, but as MemoryScope, I don't have access to personal information about individuals unless it has been shared with me during our conversation. Therefore, I cannot determine if you play any musical instruments. If you do play an instrument, feel free to share that information with me!\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I play any musical instruments?\")\n",
+ "print(\"Response 2: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I play any musical instruments?\",\n",
+ " history_message_strategy=None)\n",
+ "print(\"Response 3: \\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Memory Consolidation\n",
+ "Now, we do a bit more chatting and then try out **Memory Consolidation**."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Response 4: \n",
+ "Thank you for letting me know! Working for Meta (formerly known as Facebook) implies that you're part of a company at the forefront of technology, social media, and innovation. Meta is known for its influential platforms like Facebook, Instagram, and WhatsApp, as well as its work in virtual reality with Oculus and efforts towards building the metaverse. It's exciting to be part of a company shaping the future of digital communication and interaction.\n",
+ "Response 5: \n",
+ "Eating fruit is a great way to nourish your body and satisfy your taste buds. For this afternoon, consider having a refreshing and nutritious option like a juicy orange, a sweet apple, a handful of berries (such as strawberries, blueberries, or raspberries), or a slice of refreshing watermelon. These fruits are not only delicious but also packed with vitamins, antioxidants, and fiber to keep you energized throughout the day. Choose the one that appeals to you most or mix a few for a colorful fruit salad!\n",
+ "Response 6: \n",
+ "Watermelon is an excellent choice! It's hydrating, low in calories, and rich in nutrients like vitamin C, vitamin A, and lycopene. Its high water content makes it perfect for a refreshing snack on a warm day. Enjoy your watermelon; it's a tasty and healthy way to treat yourself this afternoon.\n",
+ "Response 7: \n",
+ "\"Happy Birthday, dear [friend's name]! May your special day be filled with love, laughter, and unforgettable moments. May the coming year bring you joy, success, and adventures that make your heart sing. Cheers to another year of friendship and wonderful memories. Enjoy your day to the fullest!\"\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"I work for Meta\")\n",
+ "print(\"Response 4: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"What fruit should I eat this afternoon?\")\n",
+ "print(\"Response 5: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"I like watermelon。\")\n",
+ "print(\"Response 6: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Help me write a short birthday wish for a friend.\")\n",
+ "print(\"Response 7: \\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: user's hobby is playing the piano (valid)\n",
+ "new observation: user works for meta (valid)\n",
+ "new observation: user likes watermelon (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_service = ms.default_memory_service\n",
+ "memory_service.init_service()\n",
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "**Memory Consolidation** extracted 3 *observations* out of the 7 chat messages from the user, with the uninformative ones being filtered out.\n",
+ "\n",
+ "We try more cases to test its time awareness and the ability to filter out fictitious contents from the user."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Response 8: \n",
+ "Prospects at Amazon can be quite promising, given its status as one of the world's leading tech companies. You can expect competitive compensation, career growth opportunities, a dynamic work environment, and the chance to work on innovative projects. However, actual experiences may vary based on role, team, and individual performance. It's always a good idea to research specific positions, company culture, and employee reviews to get a more comprehensive understanding.\n",
+ "Response 9: \n",
+ "Great, planning a business trip to Seattle can be exciting. Some key points to consider for your trip:\n",
+ "\n",
+ "1. **Weather**: Check the weather forecast for Seattle to pack appropriately. Seattle is known for its mild, marine climate with possible rainfall throughout the year.\n",
+ "\n",
+ "2. **Transportation**: Familiarize yourself with transportation options like Sea-Tac Airport, light rail, buses, and ride-sharing services for getting around.\n",
+ "\n",
+ "3. **Accommodation**: Book a hotel near your meeting location or in a convenient area like Downtown Seattle for easy access to amenities.\n",
+ "\n",
+ "4. **Meetings**: Confirm all meeting schedules and locations in advance. Consider any time differences if coming from outside the Pacific Time Zone.\n",
+ "\n",
+ "5. **Business Etiquette**: Brush up on general business etiquette, especially if you're meeting with new clients or partners.\n",
+ "\n",
+ "6. **Networking**: Look out for industry events or networking opportunities during your stay.\n",
+ "\n",
+ "7. **Dining**: Research dining options for client meetings or personal meals, Seattle offers a diverse culinary scene.\n",
+ "\n",
+ "8. **Leisure**: If time permits, plan to explore local attractions like Pike Place Market, Space Needle, or take a stroll along Puget Sound.\n",
+ "\n",
+ "Safe travels and have a productive trip!\n",
+ "Response 10: \n",
+ "That sounds like a nice plan! Catching up with Liam over a meal after his return from Google in New York will likely be an opportunity to exchange experiences and stories. Here are a few tips for your meet-up:\n",
+ "\n",
+ "1. **Choose a Venue**: Pick a restaurant that suits both your tastes and preferences, perhaps somewhere central or with a special significance to your friendship.\n",
+ "\n",
+ "2. **Schedule**: Coordinate a date and time that works well for both of your schedules, considering Liam might need time to adjust after his return.\n",
+ "\n",
+ "3. **Conversation Topics**: Prepare some conversation starters about his experience at Google, life in New York, and any shared interests or memories from your time in class together.\n",
+ "\n",
+ "4. **Professional Insights**: Liam might have valuable insights from his work at Google that could be beneficial for your own career growth, especially considering your interest in the tech industry.\n",
+ "\n",
+ "5. **Personal Updates**: Share your own updates too – your work at Meta, your love for watermelon, and your piano hobby could spark interesting discussions.\n",
+ "\n",
+ "Enjoy your reunion and the chance to learn from each other's experiences!\n",
+ "Response 11: \n",
+ "As Cynthia hugged her tightly, User felt a bittersweet pang. \"Promise we'll stay close,\" User whispered, staring at the luggage adorned with University of Iowa stickers. Cynthia smiled, eyes glistening, \"Distance means nothing when hearts are intertwined.\" They laughed, remembering late-night study sessions fueled by watermelon slices, and the echoes of piano melodies that filled User's apartment. As the cab honked, their hands reluctantly parted, but their dreams intertwined, ready for chapters anew.\n",
+ "Response 12: \n",
+ "I'm unable to provide real-time information or look up specific companies without prior data. If SMCI is a hypothetical or fictional company within our conversation context, I don't have details on it. For actual companies, it would be best to search online or refer to official sources for the most accurate and up-to-date information on their nature and activities.\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"What are the prospects like if I go to work at Amazon?\")\n",
+ "print(\"Response 8: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Note: I am planning a business trip to Seattle next week.\")\n",
+ "print(\"Response 9: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"My classmate Liam is currently working at Google. He will return to New York next month, and I plan to have a meal with him.\")\n",
+ "print(\"Response 10: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Cynthia is my best friend, and she has decided to go to university in Iowa. Write an 80-word micro drama starting with this.\")\n",
+ "print(\"Response 11: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"What kind of company is SMCI, and what do they do?\")\n",
+ "print(\"Response 12: \\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: user is interested in career prospects at amazon (valid)\n",
+ "new observation: user is inquiring about smci's nature and business (valid)\n",
+ "new observation: user is planning a business trip to seattle next week (Inference time: next week after august 2, 2024) (valid)\n",
+ "new observation: user plans to meet classmate liam in new york next month (Inference time: next month after august 2, 2024) (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "We can see **Memory Consolidation** successfully filtered out fictitious contents, and shows good time sensitivity.\n",
+ "\n",
+ "We try more cases to test its resolution of conflicting contents."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Response 13: \n",
+ "You might enjoy eating watermelon this afternoon since you like it.\n",
+ "Response 14: \n",
+ "Great choice! Enjoy your mangoes today.\n",
+ "Response 15: \n",
+ "Congratulations on your new job at Apple! How are you finding the experience so far?\n",
+ "Response 16: \n",
+ "That's wonderful! Peaches and apples are both delicious and nutritious choices. Enjoy snacking on them whenever you like.\n",
+ "Response 17: \n",
+ "No problem at all, everyone has their preferences. If you don't like coconuts, there are plenty of other fruits to enjoy.\n",
+ "Response 18: \n",
+ "Sounds like an exciting plan! Florida offers great surfing spots with its warm waters and waves. Have a fantastic time surfing next month.\n",
+ "Response 19:\n",
+ "Happy Birthday for tomorrow! May your day be filled with joy, laughter, and memorable moments.\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"What fruit should I eat this afternoon?\")\n",
+ "print(\"Response 13: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Watermelon is really good, but I also like mangoes. Today, I want to eat mangoes.\")\n",
+ "print(\"Response 14: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"I recently switched jobs and joined Apple.\")\n",
+ "print(\"Response 15: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"I also like eating peaches and apples.\")\n",
+ "print(\"Response 16: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"I don't like eating coconuts.\")\n",
+ "print(\"Response 17: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"I am planning to go surfing in Florida next month.\")\n",
+ "print(\"Response 18: \\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"Tomorrow is my birthday\")\n",
+ "print(\"Response 19:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: user likes mangoes and wants to eat them today (valid)\n",
+ "new observation: user is planning to go surfing in florida next month (valid)\n",
+ "new observation: user's birthday is tomorrow (valid)\n",
+ "new observation: user recently switched jobs and joined apple (valid)\n",
+ "new observation: user likes eating peaches and apples (valid)\n",
+ "new observation: user doesn't like eating coconuts (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Reflection and Re-Consolidation\n",
+ "Now, we have accumulated enough new *observations* in the system, so we can call **Reflection and Re-Consolidation**, let's see what will it get."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new insight: The Travel plans of User: travel plans include a business trip to seattle next week after august 2, 2024, meeting a classmate in new york next month after august 2, 2024, and going surfing in florida next month (valid)\n",
+ "new insight: The Fruit preferences of User: fruit preferences: watermelon, peaches, apples, mangoes; dislikes coconuts (valid)\n",
+ "new insight: The Career interest of User: amazon (valid)\n",
+ "modified observation: user is planning a business trip to seattle next week (Inference time: next week after august 2, 2024) (valid)\n",
+ "modified observation: user likes mangoes and wants to eat them today (valid)\n",
+ "modified observation: user is interested in career prospects at amazon (valid)\n",
+ "modified observation: user is inquiring about smci's nature and business (valid)\n",
+ "modified observation: user recently switched jobs and joined apple (valid)\n",
+ "modified observation: user's hobby is playing the piano (valid)\n",
+ "modified observation: user is planning to go surfing in florida next month (valid)\n",
+ "modified observation: user's birthday is tomorrow (valid)\n",
+ "modified observation: user likes watermelon (valid)\n",
+ "modified observation: user works for meta (valid)\n",
+ "modified observation: user plans to meet classmate liam in new york next month (Inference time: next month after august 2, 2024) (valid)\n",
+ "modified observation: user likes eating peaches and apples (valid)\n",
+ "modified observation: user doesn't like eating coconuts (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.reflect_and_reconsolidate()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Low response-time (RT) for the user\n",
+ "Finally, we test the RT of MemoryScope system for the user. Specifically, we test the difference of RT when responding with and without retrieving memory pieces from the system."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I play any musical instruments?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"With memory retrieval\\nResponse 20: \\n\" + response.message.content + f\"\\n RT: {total_time} seconds\\n\")\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I have any plans in the next month?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"With memory retrieval\\nResponse 21: \\n\" + response.message.content + f\"\\n RT: {total_time} seconds\\n\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "With memory retrieval\n",
+ "Response 20: \n",
+ "Yes, you play the piano.\n",
+ " RT: 1.494797706604004 seconds\n",
+ "\n",
+ "With memory retrieval\n",
+ "Response 21: \n",
+ "Yes, you have the following plans in the next month:\n",
+ "\n",
+ "1. A business trip to Seattle sometime after August 2, 2024.\n",
+ "2. Meeting a classmate in New York next month, also after August 2, 2024.\n",
+ "3. Going surfing in Florida next month.\n",
+ " RT: 6.400442123413086 seconds\n",
+ "\n",
+ "Without memory retrieval\n",
+ "Response 22: \n",
+ "I'm sorry, but as MemoryScope, I don't have access to personal information about individuals unless it has been shared with me during our conversation. Therefore, I cannot determine if you play any musical instruments without you providing that information. Have you mentioned anything about your musical abilities before?\n",
+ " RT: 3.8412117958068848 seconds\n",
+ "\n",
+ "Without memory retrieval\n",
+ "Response 23: \n",
+ "I'm sorry, but as an AI, I don't have access to personal schedules or information unless it has been shared with me during our conversation, which we haven't had. I cannot access external databases or personal calendars. I'm here to provide general information and assistance based on the knowledge I've been trained on.\n",
+ " RT: 6.7565529346466064 seconds\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_chat.run_service_operation(\"delete_all\")\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I play any musical instruments?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"Without memory retrieval\\nResponse 22: \\n\" + response.message.content + f\"\\n RT: {total_time} seconds\\n\")\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"Do you know if I have any plans in the next month?\\n\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"Without memory retrieval\\nResponse 23: \\n\" + response.message.content + f\"\\n RT: {total_time} seconds\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "We can see responding with retrieving memory pieces from MemoryScope does not increase RT."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## More Examples\n",
+ "We direct the reader to [Advanced Customization](../advance/custom_operator.md) for guidance on customizing the various settings of the MemoryScope system. It is also possible to create or customize your own MemoryScope **operations** by specifying a **workflow** and the corresponding **workers** that best meet your specific needs.\n",
+ "\n",
+ "Additionally, you can also try out the [CLI with a MemoryScope Chatbot](../cli/CLI_README.md). We have implemented the chatbot so that the **Memory Consolidation** and **Reflection and Re-Consolidation** operations are always run asynchronously in the backend, ensuring that they do not incur any response time for the user.\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/memoryscope/examples/api/simple_usages_zh.ipynb b/memoryscope/examples/api/simple_usages_zh.ipynb
new file mode 100644
index 00000000..a47c97ef
--- /dev/null
+++ b/memoryscope/examples/api/simple_usages_zh.ipynb
@@ -0,0 +1,488 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# chat 和 service 接口的示例用法\n",
+ "这个笔记本展示了 MemoryScope 的 **chat** 和 **service** 接口的简单用法,以及它的主要功能。\n",
+ "\n",
+ "在运行这个笔记本之前,请先按照 Readme 中的 [**Installation**](../../docs/installation_zh.md#三通过-pypi-安装) 指南进行安装,并启动 Docker 镜像。"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 初始化一个 MemoryScope 实例\n",
+ "首先,我们需要指定一个配置并初始化一个 MemoryScope 实例。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from memoryscope import MemoryScope, Arguments\n",
+ "arguments = Arguments(\n",
+ " language=\"cn\",\n",
+ " human_name=\"用户\",\n",
+ " assistant_name=\"AI\",\n",
+ " memory_chat_class=\"api_memory_chat\",\n",
+ " generation_backend=\"dashscope_generation\",\n",
+ " generation_model=\"qwen-max\",\n",
+ " embedding_backend=\"dashscope_embedding\",\n",
+ " embedding_model=\"text-embedding-v2\",\n",
+ " rank_backend=\"dashscope_rank\",\n",
+ " rank_model=\"gte-rerank\",\n",
+ " enable_ranker=True)\n",
+ "\n",
+ "ms = MemoryScope(arguments=arguments)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 聊天(不含记忆)\n",
+ "MemoryScope 配有默认的 chat 接口,因此开始聊天非常容易,就像使用任何大型语言模型聊天机器人一样。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "回答1:\n",
+ "很高兴了解到您的爱好是弹琴,这是一种既能陶冶情操又能提升音乐技能的美妙艺术形式。无论是古典钢琴、爵士乐还是现代流行曲目,每一种风格都能带来不同的享受和挑战。希望您在弹琴的过程中能够持续发现乐趣,创造出更多动人的旋律。\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_chat = ms.default_memory_chat\n",
+ "memory_chat.run_service_operation(\"delete_all\")\n",
+ "response = memory_chat.chat_with_memory(query=\"我的爱好是弹琴。\")\n",
+ "print(\"回答1:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "你可以选择进行含有或不含有多轮对话上下文的聊天。然而,由于尚未调用**记忆巩固**功能,系统中还没有任何记忆片段。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "回答2:\n",
+ "是的,您提到过您的爱好是弹琴,所以我认为您对键盘乐器,特别是钢琴有一定的爱好。\n",
+ "回答3:\n",
+ "作为基于当前对话的MemoryScope智能助理,我没有之前关于您乐器爱好的信息。请告诉我,您喜欢哪种乐器?这样我就可以记住并提供相关帮助了。\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"你知道我有什么乐器爱好吗?\")\n",
+ "print(\"回答2:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"你知道我有什么乐器爱好吗?\",\n",
+ " history_message_strategy=None)\n",
+ "print(\"回答3:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## **记忆巩固**\n",
+ "现在,我们再聊多几句,然后尝试**记忆巩固**功能。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "回答4:\n",
+ "了解,您在阿里巴巴工作。阿里巴巴集团是一家总部位于中国杭州的全球领先的电子商务和科技公司,以其电子商务平台如淘宝、天猫闻名,同时在云计算、数字媒体及娱乐、金融科技等领域也有广泛布局。如果您有关于工作、技术或公司文化方面的问题,欢迎随时询问。\n",
+ "回答5:\n",
+ "选择水果可以根据个人口味、营养需求以及季节来决定。夏天,一些清爽解暑的水果会是不错的选择,比如西瓜、哈密瓜、葡萄、桃子或者李子,它们都含有丰富的水分和维生素,有助于消暑降温。如果您想要补充纤维素,火龙果或者猕猴桃也是很好的选择。最终,选择哪种水果,还是要看您自己的喜好和身体状况。\n",
+ "回答6:\n",
+ "西瓜是夏季的理想选择,它不仅清凉解渴,还含有大量的水分和电解质,可以帮助身体补充流失的水分。西瓜还富含维生素C、A和抗氧化剂,如番茄红素,对皮肤健康和心血管系统都有益处。享用美味的西瓜时,记得切块后冷藏一下,口感会更加清爽哦!\n",
+ "回答7:\n",
+ "\"生日快乐,愿你的每一天都如蛋糕般甜蜜,笑容比烛光更灿烂!\"\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"我在阿里巴巴干活\")\n",
+ "print(\"回答4:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"今天下午吃什么水果好?\")\n",
+ "print(\"回答5:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我喜欢吃西瓜。\")\n",
+ "print(\"回答6:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"帮我写一句给朋友的生日祝福语,简短一点。\")\n",
+ "print(\"回答7:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: 用户爱好弹琴。 (valid)\n",
+ "new observation: 用户在阿里巴巴工作。 (valid)\n",
+ "new observation: 用户喜欢吃西瓜。 (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_service = ms.default_memory_service\n",
+ "memory_service.init_service()\n",
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "**记忆巩固**从用户的7条聊天消息中提取了3条 *observations* ,其余无效的信息被过滤掉了。\n",
+ "\n",
+ "我们尝试更多的情况,以测试其时间感知能力和过滤用户虚构的内容的能力。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "回答8:\n",
+ "京东作为中国领先的电商平台之一,拥有广泛的业务范围和良好的行业声誉。加入京东工作,您可能会享受到以下几方面的前景优势:\n",
+ "\n",
+ "1. **职业发展**:京东提供多元化的职业路径和晋升机会,您可以在电商、物流、科技、金融等多个领域找到适合自己的位置和发展空间。\n",
+ "\n",
+ "2. **技术与创新**:京东持续投入于技术创新,尤其是在人工智能、大数据、云计算等方面,为员工提供了接触和学习前沿技术的机会。\n",
+ "\n",
+ "3. **企业文化**:京东强调“正道成功”的企业文化,注重诚信、团队合作与社会责任,有利于塑造积极向上的工作氛围。\n",
+ "\n",
+ "4. **福利待遇**:京东通常为员工提供有竞争力的薪酬福利体系,包括但不限于健康保险、员工培训、股权激励等。\n",
+ "\n",
+ "5. **行业影响力**:作为行业巨头,京东的从业经历对您的职业生涯将是一大亮点,有助于提升个人品牌和未来的职业选择灵活性。\n",
+ "\n",
+ "当然,具体前景还取决于您的职位、个人能力、行业趋势及个人职业规划等因素。建议您深入了解目标岗位的具体要求,评估个人与岗位的匹配度,并关注行业动态,为自己的职业发展做出合理规划。\n",
+ "回答9:\n",
+ "好的,已记录您计划下周去北京出差。请随时告诉我如果您需要关于北京的出行建议、天气预报或是其他相关信息。祝您出差顺利!\n",
+ "回答10:\n",
+ "已记录您的安排,您将于下个月与在亚马逊工作的同学李亚平在上海共进晚餐。临近日期时,不妨提前联系李亚平确认具体时间和地点,以便愉快地进行聚会。希望你们有个美好的重聚!\n",
+ "回答11:\n",
+ "【场景:傍晚,公园长椅】 \n",
+ "小亮是我最好的朋友,他决定去山西上大学。夕阳下,我们肩并肩坐着。 \n",
+ "我:“山西的面食可出名了,你这小吃货有福了!” \n",
+ "小亮笑:“那必须的,说好你放假就来找我,咱们一起吃遍山西!” \n",
+ "我点头,心中泛起不舍:“一言为定,别忘了,那里还有千年古城等你探索。” \n",
+ "小亮看向远方,眼里闪烁着梦想的光:“新旅程,我们一起加油!” \n",
+ "【画面渐暗,友情的力量温暖而坚定】\n",
+ "回答12:\n",
+ "SMCI可能指代的是Super Micro Computer, Inc.(超微电脑股份有限公司),简称Supermicro。这是一家总部位于美国加利福尼亚州圣何塞的公司,成立于1993年。Supermicro主要设计、制造和销售高性能服务器和技术解决方案,包括服务器、存储系统、主板以及支持云计算、数据中心、企业IT、高性能计算(HPC)和嵌入式系统的其他硬件组件。它们的产品以高效率、灵活性和定制化选项著称,在全球范围内服务于各种规模的企业和组织。\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"假如我去京东工作,前景怎么样?\")\n",
+ "print(\"回答8:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"记一下,下周我准备去北京出差\")\n",
+ "print(\"回答9:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我同学李亚平现在在亚马逊工作,他下个月回上海,我要和他吃个饭\")\n",
+ "print(\"回答10:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"小亮是我最好的朋友,他决定去山西上大学。以这个为开头写一个80字的微剧本。\")\n",
+ "print(\"回答11:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"SMCI是什么公司,做什么的?\")\n",
+ "print(\"回答12:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: 用户计划2024年8月9日去北京出差。 (推断时间: 2024年8月9日) (valid)\n",
+ "new observation: 用户的同学李亚平下个月回上海,用户将与其见面吃饭。 (推断时间: 2024年9月) (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "我们可以看到,**记忆巩固**成功过滤掉了虚假内容,并展示了良好的时间敏感性。\n",
+ "\n",
+ "我们尝试更多的情况,以测试其解决冲突内容的能力。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "回答13:\n",
+ "既然您喜欢吃西瓜,今天下午吃西瓜是个不错的选择。\n",
+ "回答14:\n",
+ "那太好了,如果您今天想换换口味,吃芒果也是很好的选择,享受它的香甜吧!\n",
+ "回答15:\n",
+ "恭喜您加入美团!希望您的新工作一切顺利,有新的挑战和机遇。\n",
+ "回答16:\n",
+ "了解了,桃子和苹果都是既营养又美味的选择,多吃水果对身体有益,您可以根据季节和个人喜好来挑选。\n",
+ "回答17:\n",
+ "好的,知道您不喜欢椰子,以后在推荐水果时会留意这一点。\n",
+ "回答18:\n",
+ "听起来很有趣!去海南冲浪是个很棒的计划,下个月那边的天气应该很适合水上活动,祝您玩得开心!别忘了做好防晒哦。\n",
+ "回答19:\n",
+ "生日快乐!希望您明天能度过一个特别且难忘的一天,满满的祝福给您!有任何庆祝计划吗?\n"
+ ]
+ }
+ ],
+ "source": [
+ "response = memory_chat.chat_with_memory(query=\"今天下午吃什么水果好?\")\n",
+ "print(\"回答13:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"西瓜确实不错,但是我也喜欢吃芒果。我今天想吃芒果。\")\n",
+ "print(\"回答14:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我最近跳槽去了美团。\")\n",
+ "print(\"回答15:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我还喜欢吃桃子和苹果。\")\n",
+ "print(\"回答16:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我不喜欢吃椰子。\")\n",
+ "print(\"回答17:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"我准备下个月去海南冲浪。\")\n",
+ "print(\"回答18:\\n\" + response.message.content)\n",
+ "response = memory_chat.chat_with_memory(query=\"明天是我生日。\")\n",
+ "print(\"回答19:\\n\" + response.message.content)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new observation: 用户喜欢吃桃子和苹果。 (valid)\n",
+ "new observation: 用户不喜欢吃椰子。 (valid)\n",
+ "new observation: 用户喜欢吃芒果。 (valid)\n",
+ "new observation: 用户计划2024年9月去海南冲浪。 (推断时间: 2024年9月) (valid)\n",
+ "new observation: 用户的生日是每年8月3日。 (推断时间: 每年8月3日) (valid)\n",
+ "modified observation: 用户在阿里巴巴工作。 (expired)\n",
+ "modified observation: 用户最近跳槽至美团。 (expired)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.consolidate_memory()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## **反思与再巩固**\n",
+ "现在,我们在系统中已经积累了足够多的新的 *observations* ,因此我们可以调用**反思与再巩固**功能,让我们看看会得到什么。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "consolidate_memory result=[MEMORY ACTIONS]:\n",
+ "new insight: 用户的出差计划: 2024年8月9日去北京出差 (valid)\n",
+ "new insight: 用户的生日: 每年8月3日 (valid)\n",
+ "new insight: 用户的水果偏好: 喜欢桃子、苹果、西瓜、芒果,不喜欢吃椰子 (valid)\n",
+ "modified observation: 用户计划2024年8月9日去北京出差。 (推断时间: 2024年8月9日) (valid)\n",
+ "modified observation: 用户的生日是每年8月3日。 (推断时间: 每年8月3日) (valid)\n",
+ "modified observation: 用户计划2024年9月去海南冲浪。 (推断时间: 2024年9月) (valid)\n",
+ "modified observation: 用户喜欢吃芒果。 (valid)\n",
+ "modified observation: 用户喜欢吃桃子和苹果。 (valid)\n",
+ "modified observation: 用户爱好弹琴。 (valid)\n",
+ "modified observation: 用户喜欢吃西瓜。 (valid)\n",
+ "modified observation: 用户不喜欢吃椰子。 (valid)\n",
+ "modified observation: 用户的同学李亚平下个月回上海,用户将与其见面吃饭。 (推断时间: 2024年9月) (valid)\n"
+ ]
+ }
+ ],
+ "source": [
+ "result = memory_service.reflect_and_reconsolidate()\n",
+ "print(f\"consolidate_memory result={result}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 低用户时延(RT)\n",
+ "\n",
+ "最后,我们测试 MemoryScope 系统对用户的响应时间 (RT)。具体来说,我们测试在有和没有从系统中检索记忆片段时聊天的响应时间的差异。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"你知道我的乐器爱好是什么吗?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"使用记忆检索\\n回答20:\\n\" + response.message.content + f\"\\n 耗时:{total_time}秒\\n\")\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"你知道我接下去的一个月内有什么计划吗?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"使用记忆检索\\n回答21:\\n\" + response.message.content + f\"\\n 耗时:{total_time}秒\\n\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "使用记忆检索\n",
+ "回答20:\n",
+ "您喜欢弹琴。\n",
+ " 耗时:1.3783161640167236秒\n",
+ "\n",
+ "使用记忆检索\n",
+ "回答21:\n",
+ "您接下来一个月内的计划包括:\n",
+ "- 2024年8月9日去北京出差。\n",
+ "- 计划在2024年9月去海南冲浪。\n",
+ "- 2024年9月,您的同学李亚平回上海,您将与他见面吃饭。\n",
+ " 耗时:6.538439035415649秒\n",
+ "\n",
+ "不使用记忆检索\n",
+ "回答20:\n",
+ "对不起,我没有记录您的个人信息,包括您的乐器爱好。如果您告诉我,我可以帮您记住。\n",
+ " 耗时:2.597784996032715秒\n",
+ "\n",
+ "不使用记忆检索\n",
+ "回答21:\n",
+ "对不起,作为基于当前会话的MemoryScope智能助理,我无法获取或存储您的个人日程信息。如果您需要查询自己的计划,建议您查看自己的日历或者备忘录。\n",
+ " 耗时:5.246160984039307秒\n"
+ ]
+ }
+ ],
+ "source": [
+ "memory_chat.run_service_operation(\"delete_all\")\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"你知道我的乐器爱好是什么吗?\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"不使用记忆检索\\n回答20:\\n\" + response.message.content + f\"\\n 耗时:{total_time}秒\\n\")\n",
+ "\n",
+ "start_time = time.time()\n",
+ "response = memory_chat.chat_with_memory(query=\"你知道我接下去的一个月内有什么计划吗?\\n\",\n",
+ " history_message_strategy=None)\n",
+ "end_time = time.time()\n",
+ "total_time = end_time - start_time\n",
+ "print(\"不使用记忆检索\\n回答21:\\n\" + response.message.content + f\"\\n 耗时:{total_time}秒\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "我们可以看到,从 MemoryScope 检索记忆片段不会增加聊天的响应时间。"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 更多用法\n",
+ "我们建议读者参考[进阶自定义用法](../advance/custom_operator_zh.md)来对MemoryScope系统进行各种自定义设置。您还可以通过自定义**workflow**和对应的**worker**来创建或定制满足您特定需求的**operation**。\n",
+ "\n",
+ "此外,您还可以尝试使用[在命令行与MemoryScope聊天机器人交互](../cli/CLI_README_ZH.md)。我们在这里实现了始终在后台异步运行**记忆巩固**和**反思与再巩固**这两个操作,从而使得它们不会增加聊天的响应时间。"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/memoryscope/examples/cli/CLI_README.md b/memoryscope/examples/cli/CLI_README.md
new file mode 100644
index 00000000..f247ff4e
--- /dev/null
+++ b/memoryscope/examples/cli/CLI_README.md
@@ -0,0 +1,64 @@
+# The Cli Interface of MemoryScope
+
+## Usage
+Before running, follow the [**Installation**](../../docs/installation.md#iii-install-from-pypi) guidelines in Readme, and start the Docker image first.
+MemoryScope can be launched in two different ways:
+
+### 1. Using YAML Configuration File
+
+If you prefer to configure your settings via a YAML file, you can do so by providing the path to the configuration file as follows:
+```bash
+memoryscope --config_path=memoryscope/core/config/demo_config.yaml
+```
+
+### 2. Using Command Line Arguments
+
+Alternatively, you can specify all the parameters directly on the command line:
+
+```bash
+# Chinese / Dashscope
+memoryscope --language="cn" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="用户" \
+ --assistant_name="AI" \
+ --generation_backend="dashscope_generation" \
+ --generation_model="qwen-max" \
+ --embedding_backend="dashscope_embedding" \
+ --embedding_model="text-embedding-v2" \
+ --enable_ranker=True \
+ --rank_backend="dashscope_rank" \
+ --rank_model="gte-rerank"
+# English / OpenAI
+memoryscope --language="en" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="user" \
+ --assistant_name="AI" \
+ --generation_backend="openai_generation" \
+ --generation_model="gpt-4o" \
+ --embedding_backend="openai_embedding" \
+ --embedding_model="text-embedding-3-small" \
+ --enable_ranker=False
+```
+
+Here are the available options that can be set through either method:
+
+- `--language`: The language used for the conversation.
+- `--memory_chat_class`: The class name for managing the chat history.
+- `--human_name`: The name of the human user.
+- `--assistant_name`: The name of the AI assistant.
+- `--generation_backend`: The backend used for generating responses.
+- `--generation_model`: The model used for generating responses.
+- `--embedding_backend`: The backend used for text embeddings.
+- `--embedding_model`: The model used for creating text embeddings.
+- `--enable_ranker`: A boolean indicating whether to use a dummy ranker (default is `False`).
+- `--rank_backend`: The backend used for ranking responses.
+- `--rank_model`: The model used for ranking responses.
+
+### 3. View Memory
+You can open two command line windows following the method in the second step.
+In one command line window, you can have a conversation with the AI, while in the other, you can check the AI's long-term memory about the user.
+Use /help to open the command line help, and find the command /list_memory along with the corresponding auto-refresh instruction.
+```
+/list_memory refresh_time=5
+```
+Then you can enjoy a pleasant conversation with the AI!
\ No newline at end of file
diff --git a/memoryscope/examples/cli/CLI_README_ZH.md b/memoryscope/examples/cli/CLI_README_ZH.md
new file mode 100644
index 00000000..b9debdda
--- /dev/null
+++ b/memoryscope/examples/cli/CLI_README_ZH.md
@@ -0,0 +1,64 @@
+# MemoryScope 的命令行接口
+
+## 使用方法
+在运行之前,请先按照 Readme 中的 [**Installation**](../../docs/installation_zh.md#三通过-pypi-安装) 指南进行安装,并启动 Docker 镜像。
+MemoryScope 可以通过两种不同的方式启动:
+
+### 1. 使用 YAML 配置文件
+
+如果您更喜欢通过 YAML 文件配置设置,可以通过提供配置文件的路径来实现:
+```bash
+memoryscope --config_path=memoryscope/core/config/demo_config_zh.yaml
+```
+
+### 2. 使用命令行参数
+
+或者,您可以直接在命令行上指定所有参数:
+
+```
+# 中文
+memoryscope --language="cn" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="用户" \
+ --assistant_name="AI" \
+ --generation_backend="dashscope_generation" \
+ --generation_model="qwen-max" \
+ --embedding_backend="dashscope_embedding" \
+ --embedding_model="text-embedding-v2" \
+ --enable_ranker=True \
+ --rank_backend="dashscope_rank" \
+ --rank_model="gte-rerank"
+# 英文
+memoryscope --language="en" \
+ --memory_chat_class="cli_memory_chat" \
+ --human_name="User" \
+ --assistant_name="AI" \
+ --generation_backend="openai_generation" \
+ --generation_model="gpt-4o" \
+ --embedding_backend="openai_embedding" \
+ --embedding_model="text-embedding-3-small" \
+ --enable_ranker=False
+```
+
+以下是可以通过任一方法设置的可用选项:
+
+- `--language`: 对话中使用的语言。
+- `--memory_chat_class`: 管理聊天记录的类名。
+- `--human_name`: 人类用户的名字。
+- `--assistant_name`: AI 助手的名字。
+- `--generation_backend`: 用于生成回复的后端。
+- `--generation_model`: 用于生成回复的模型。
+- `--embedding_backend`: 用于文本嵌入的后端。
+- `--embedding_model`: 用于创建文本嵌入的模型。
+- `--enable_ranker`: 一个布尔值,指示是否使用排名器(默认为 False)。
+- `--rank_backend`: 用于排名回复的后端。
+- `--rank_model`: 用于排名回复的模型。
+
+### 3. 查看记忆
+按照第二步的方式可以打开两个命令行的窗口。
+其中一个命令行窗口可以和AI进行对话,另一个命令行窗口可以查看AI关于用户的长期记忆
+使用/help打开命令行帮助,找到/list_memory的命令和对应自动刷新的指令。
+```
+/list_memory refresh_time=5
+```
+接下来就可以和AI进行愉快地交流啦。
\ No newline at end of file
diff --git a/memoryscope/examples/docker/entrypoint.sh b/memoryscope/examples/docker/entrypoint.sh
new file mode 100644
index 00000000..c60739e6
--- /dev/null
+++ b/memoryscope/examples/docker/entrypoint.sh
@@ -0,0 +1,2 @@
+sh examples/docker/run_elastic_search.sh
+python quick-start-demo.py --config_path=memoryscope/core/config/demo_config_zh.yaml
\ No newline at end of file
diff --git a/memoryscope/examples/docker/run_elastic_search.sh b/memoryscope/examples/docker/run_elastic_search.sh
new file mode 100644
index 00000000..0ee5c0cb
--- /dev/null
+++ b/memoryscope/examples/docker/run_elastic_search.sh
@@ -0,0 +1 @@
+su - elastic_search_user -c "/home/elastic_search_user/elastic_search/elasticsearch-8.15.0/bin/elasticsearch -E xpack.security.enabled=false -E discovery.type=single-node -E xpack.license.self_generated.type=trial -d"
diff --git a/memoryscope/memoryscope/__init__.py b/memoryscope/memoryscope/__init__.py
new file mode 100644
index 00000000..a5d21147
--- /dev/null
+++ b/memoryscope/memoryscope/__init__.py
@@ -0,0 +1,10 @@
+""" Version of MemoryScope."""
+__version__ = "0.1.1.0"
+import fire
+
+from memoryscope.core.config.arguments import Arguments # noqa: F401
+from memoryscope.core.memoryscope import MemoryScope # noqa: F401
+
+
+def cli():
+ fire.Fire(MemoryScope.cli_memory_chat)
diff --git a/memoryscope/memoryscope/constants/__init__.py b/memoryscope/memoryscope/constants/__init__.py
new file mode 100644
index 00000000..7ef41980
--- /dev/null
+++ b/memoryscope/memoryscope/constants/__init__.py
@@ -0,0 +1,8 @@
+from . import common_constants
+from . import language_constants
+
+
+__all__ = [
+ "common_constants",
+ "language_constants"
+]
diff --git a/memoryscope/memoryscope/constants/common_constants.py b/memoryscope/memoryscope/constants/common_constants.py
new file mode 100644
index 00000000..74645416
--- /dev/null
+++ b/memoryscope/memoryscope/constants/common_constants.py
@@ -0,0 +1,48 @@
+# common_constants.py
+# This module defines constants used as keys throughout the application to maintain a consistent reference
+# for data structures related to workflow management, chat interactions, context storage, memory operations,
+# node processing, and temporal inference functionalities.
+
+WORKFLOW_NAME = "workflow_name"
+
+MEMORYSCOPE_CONTEXT = "memoryscope_context"
+
+RESULT = "result"
+
+MEMORIES = "memories"
+
+CHAT_MESSAGES = "chat_messages"
+
+CHAT_MESSAGES_SCATTER = "chat_messages_scatter"
+
+CHAT_KWARGS = "chat_kwargs"
+
+USER_NAME = "user_name"
+
+TARGET_NAME = "target_name"
+
+MEMORY_MANAGER = "memory_manager"
+
+QUERY_WITH_TS = "query_with_ts"
+
+RETRIEVE_MEMORY_NODES = "retrieve_memory_nodes"
+
+RANKED_MEMORY_NODES = "ranked_memory_nodes"
+
+NOT_REFLECTED_NODES = "not_reflected_nodes"
+
+NOT_UPDATED_NODES = "not_updated_nodes"
+
+EXTRACT_TIME_DICT = "extract_time_dict"
+
+NEW_OBS_NODES = "new_obs_nodes"
+
+NEW_OBS_WITH_TIME_NODES = "new_obs_with_time_nodes"
+
+INSIGHT_NODES = "insight_nodes"
+
+TODAY_NODES = "today_nodes"
+
+MERGE_OBS_NODES = "merge_obs_nodes"
+
+TIME_INFER = "time_infer"
diff --git a/memoryscope/memoryscope/constants/language_constants.py b/memoryscope/memoryscope/constants/language_constants.py
new file mode 100644
index 00000000..e3ccf952
--- /dev/null
+++ b/memoryscope/memoryscope/constants/language_constants.py
@@ -0,0 +1,215 @@
+from memoryscope.enumeration.language_enum import LanguageEnum
+
+# This dictionary maps languages to lists of words related to datetime expressions.
+# It aids in recognizing and processing datetime mentions in text, enhancing the system's ability to understand
+# temporal context across different languages.
+DATATIME_WORD_LIST = {
+ LanguageEnum.CN: [
+ "天",
+ "周",
+ "月",
+ "年",
+ "星期",
+ "点",
+ "分钟",
+ "小时",
+ "秒",
+ "上午",
+ "下午",
+ "早上",
+ "早晨",
+ "晚上",
+ "中午",
+ "日",
+ "夜",
+ "清晨",
+ "傍晚",
+ "凌晨",
+ "岁",
+ ],
+ LanguageEnum.EN: [
+ # Units of Time
+ "year", "yr",
+ "month", "mo",
+ "week", "wk",
+ "day", "d",
+ "hour", "hr",
+ "minute", "min",
+ "second", "sec",
+
+ # Days of the Week
+ "Monday", "Mon",
+ "Tuesday", "Tue", "Tues",
+ "Wednesday", "Wed",
+ "Thursday", "Thu", "Thur", "Thurs",
+ "Friday", "Fri",
+ "Saturday", "Sat",
+ "Sunday", "Sun",
+
+ # Months of the Year
+ "January", "Jan",
+ "February", "Feb",
+ "March", "Mar",
+ "April", "Apr",
+ "May", "May",
+ "June", "Jun",
+ "July", "Jul",
+ "August", "Aug",
+ "September", "Sep", "Sept",
+ "October", "Oct",
+ "November", "Nov",
+ "December", "Dec",
+
+ # Relative Time References
+ "Today",
+ "Tomorrow", "Tmrw",
+ "Yesterday", "Yday",
+ "Now",
+ "Morning", "AM", "a.m.",
+ "Afternoon", "PM", "p.m.",
+ "Evening",
+ "Night",
+ "Midnight",
+ "Noon",
+
+ # Seasonal References
+ "Spring",
+ "Summer",
+ "Autumn", "Fall",
+ "Winter",
+
+ # General Time References
+ "Century", "cent.",
+ "Decade",
+ "Millennium",
+ "Quarter", "Q1", "Q2", "Q3", "Q4",
+ "Semester",
+ "Fortnight",
+ "Weekend"
+ ]
+}
+
+# A mapping of weekdays for each supported language, facilitating calendar-related operations and understanding
+# within the application.
+WEEKDAYS = {
+ LanguageEnum.CN: [
+ "周一",
+ "周二",
+ "周三",
+ "周四",
+ "周五",
+ "周六",
+ "周日"
+ ],
+ LanguageEnum.EN: [
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+ ]
+}
+
+MONTH_DICT = {
+ LanguageEnum.CN: [
+ "1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月",
+ ],
+ LanguageEnum.EN: [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ]
+}
+
+# Constants for the word 'none' in different languages
+NONE_WORD = {
+ LanguageEnum.CN: "无",
+ LanguageEnum.EN: "none"
+}
+
+# Constants for the word 'repeated' in different languages
+REPEATED_WORD = {
+ LanguageEnum.CN: "重复",
+ LanguageEnum.EN: "repeated"
+}
+
+# Constants for the word 'contradictory' in different languages
+CONTRADICTORY_WORD = {
+ LanguageEnum.CN: "矛盾",
+ LanguageEnum.EN: "contradiction"
+}
+
+# Constants for the phrase 'included' in different languages
+CONTAINED_WORD = {
+ LanguageEnum.CN: "被包含",
+ LanguageEnum.EN: "contained"
+}
+
+# Constants for the symbol ':' in different languages' representations
+COLON_WORD = {
+ LanguageEnum.CN: ":",
+ LanguageEnum.EN: ":"
+}
+
+# Constants for the symbol ',' in different languages' representations
+COMMA_WORD = {
+ LanguageEnum.CN: ",",
+ LanguageEnum.EN: ","
+}
+
+# Default human name placeholders for different languages
+DEFAULT_HUMAN_NAME = {
+ LanguageEnum.CN: "用户",
+ LanguageEnum.EN: "user"
+}
+
+# Mapping of datetime terms from natural language to standardized keys for each supported language
+DATATIME_KEY_MAP = {
+ LanguageEnum.CN: {
+ "年": "year",
+ "月": "month",
+ "日": "day",
+ "周": "week",
+ "星期几": "weekday",
+ },
+ LanguageEnum.EN: {
+ "Year": "year",
+ "Month": "month",
+ "Day": "day",
+ "Week": "week",
+ "Weekday": "weekday",
+ }
+}
+
+# Phrase for indicating inferred time in different languages
+TIME_INFER_WORD = {
+ LanguageEnum.CN: "推断时间",
+ LanguageEnum.EN: "Inference time"
+}
+
+USER_NAME_EXPRESSION = {
+ LanguageEnum.CN: "用户姓名是{name}。",
+ LanguageEnum.EN: "User's name is {name}."
+}
diff --git a/memoryscope/memoryscope/contrib/example_query_worker.py b/memoryscope/memoryscope/contrib/example_query_worker.py
new file mode 100644
index 00000000..90821522
--- /dev/null
+++ b/memoryscope/memoryscope/contrib/example_query_worker.py
@@ -0,0 +1,86 @@
+import datetime
+
+from memoryscope.constants.common_constants import QUERY_WITH_TS
+from memoryscope.constants.language_constants import NONE_WORD
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+
+
+class ExampleQueryWorker(MemoryBaseWorker):
+ # NOTE: If you want to utilize the capabilities of the prompt handler, please be sure to include this sentence.
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.rewrite_history_count: int = kwargs.get("rewrite_history_count", 2)
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+
+ def rewrite_query(self, query: str) -> str:
+ chat_messages = self.chat_messages_scatter
+ if len(chat_messages) <= 1:
+ return query
+
+ if chat_messages[-1].role == MessageRoleEnum.USER:
+ chat_messages = chat_messages[:-1]
+ chat_messages = chat_messages[-self.rewrite_history_count:]
+
+ # get context
+ context_list = []
+ for message in chat_messages:
+ context = message.content
+ if len(context) > 200:
+ context = context[:100] + context[-100:]
+ if message.role == MessageRoleEnum.USER:
+ context_list.append(f"{self.target_name}: {context}")
+ elif message.role == MessageRoleEnum.ASSISTANT:
+ context_list.append(f"Assistant: {context}")
+
+ if not context_list:
+ return query
+
+ system_prompt = self.prompt_handler.rewrite_query_system
+ user_query = self.prompt_handler.rewrite_query_query.format(query=query,
+ context="\n".join(context_list))
+ rewrite_query_message = self.prompt_to_msg(system_prompt=system_prompt,
+ few_shot="",
+ user_query=user_query)
+ self.logger.info(f"rewrite_query_message={rewrite_query_message}")
+
+ # Invoke the LLM to generate a response
+ response = self.generation_model.call(messages=rewrite_query_message,
+ **self.generation_model_kwargs)
+
+ # Handle empty or unsuccessful responses
+ if not response.status or not response.message.content:
+ return query
+
+ response_text = response.message.content
+ self.logger.info(f"rewrite_query.response_text={response_text}")
+
+ if not response_text or response_text.lower() == self.get_language_value(NONE_WORD):
+ return query
+
+ return response_text
+
+ def _run(self):
+ query = "" # Default query value
+ timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default
+
+ if "query" in self.chat_kwargs:
+ # set query if exists
+ query = self.chat_kwargs["query"]
+ if not query:
+ query = ""
+ query = query.strip()
+
+ # set ts if exists
+ _timestamp = self.chat_kwargs.get("timestamp")
+ if _timestamp and isinstance(_timestamp, int):
+ timestamp = _timestamp
+
+ if self.rewrite_history_count > 0:
+ t_query = self.rewrite_query(query=query)
+ if t_query:
+ query = t_query
+
+ # Store the determined query and its timestamp in the context
+ self.set_workflow_context(QUERY_WITH_TS, (query, timestamp))
diff --git a/memoryscope/memoryscope/contrib/example_query_worker.yaml b/memoryscope/memoryscope/contrib/example_query_worker.yaml
new file mode 100644
index 00000000..cebab04f
--- /dev/null
+++ b/memoryscope/memoryscope/contrib/example_query_worker.yaml
@@ -0,0 +1,21 @@
+rewrite_query_system:
+ cn: |
+ 任务: 消除指代问题并重写
+ 要求: 检查提供的问题是否存在指代。如果存在指代,通过上下文信息重写问题,使其信息充足,能够单独回答。如果没有指代问题,则回答“无”。
+ en: |
+ Task: Eliminate referencing issues and rewrite
+ Requirements: Check the provided questions for any references. If references exist, rewrite the questions using contextual information to make them sufficiently informative so they can be answered independently. If there are no referencing issues, respond with "None".
+
+rewrite_query_query:
+ cn: |
+ 上下文:
+ {context}
+ 问题:{query}
+ 重写:
+
+ en: |
+ Context:
+ {context}
+ Question: {query}
+ Rewrite:
+
diff --git a/memoryscope/memoryscope/core/__init__.py b/memoryscope/memoryscope/core/__init__.py
new file mode 100644
index 00000000..bc960d98
--- /dev/null
+++ b/memoryscope/memoryscope/core/__init__.py
@@ -0,0 +1,7 @@
+from .memoryscope import MemoryScope
+from .memoryscope_context import MemoryscopeContext
+
+__all__ = [
+ "MemoryScope",
+ "MemoryscopeContext"
+]
diff --git a/memoryscope/memoryscope/core/chat/__init__.py b/memoryscope/memoryscope/core/chat/__init__.py
new file mode 100644
index 00000000..1acace24
--- /dev/null
+++ b/memoryscope/memoryscope/core/chat/__init__.py
@@ -0,0 +1,9 @@
+from .api_memory_chat import ApiMemoryChat
+from .base_memory_chat import BaseMemoryChat
+from .cli_memory_chat import CliMemoryChat
+
+__all__ = [
+ "ApiMemoryChat",
+ "BaseMemoryChat",
+ "CliMemoryChat"
+]
diff --git a/memoryscope/memoryscope/core/chat/api_memory_chat.py b/memoryscope/memoryscope/core/chat/api_memory_chat.py
new file mode 100644
index 00000000..192a577e
--- /dev/null
+++ b/memoryscope/memoryscope/core/chat/api_memory_chat.py
@@ -0,0 +1,201 @@
+from typing import List, Optional, Literal
+
+from memoryscope.constants.common_constants import MEMORIES
+from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME, USER_NAME_EXPRESSION
+from memoryscope.core.chat.base_memory_chat import BaseMemoryChat
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.models.base_model import BaseModel
+from memoryscope.core.service.base_memory_service import BaseMemoryService
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.utils.prompt_handler import PromptHandler
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.scheme.message import Message
+from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen
+
+
+class ApiMemoryChat(BaseMemoryChat):
+
+ def __init__(self,
+ memory_service: str,
+ generation_model: str,
+ context: MemoryscopeContext,
+ stream: bool = False,
+ **kwargs):
+
+ super().__init__(**kwargs)
+
+ self._memory_service: BaseMemoryService | str = memory_service
+ self._generation_model: BaseModel | str = generation_model
+ self.context: MemoryscopeContext = context
+ self.stream: bool = stream
+ self.generation_model_kwargs: dict = kwargs.pop("generation_model_kwargs", {})
+
+ self._prompt_handler: PromptHandler | None = None
+
+ @property
+ def prompt_handler(self) -> PromptHandler:
+ """
+ Lazy initialization property for the prompt handler.
+
+ This property ensures that the `_prompt_handler` attribute is only instantiated when it is first accessed.
+ It uses the current file's path and additional keyword arguments for configuration.
+
+ Returns:
+ PromptHandler: An instance of the PromptHandler configured for this CLI session.
+ """
+ if self._prompt_handler is None:
+ self._prompt_handler = PromptHandler(__file__,
+ language=self.context.language,
+ prompt_file="memory_chat_prompt",
+ **self.kwargs)
+ return self._prompt_handler
+
+ @property
+ def memory_service(self) -> BaseMemoryService:
+ """
+ Property to access the memory service. If the service is initially set as a string,
+ it will be looked up in the memory service dictionary of context, initialized,
+ and then returned as an instance of `BaseMemoryService`. Ensures the memory service
+ is properly started before use.
+
+ Returns:
+ BaseMemoryService: An active memory service instance.
+
+ Raises:
+ ValueError: If the declaration of memory service is not found in the memory service dictionary of context.
+ """
+ if isinstance(self._memory_service, str):
+ if self._memory_service not in self.context.memory_service_dict:
+ raise ValueError(f"Missing declaration of memory_service in context: {self._memory_service}")
+
+ self._memory_service: BaseMemoryService = self.context.memory_service_dict[self._memory_service]
+ # init service & update kwargs
+ self._memory_service.init_service()
+ return self._memory_service
+
+ @property
+ def human_name(self):
+ return self.memory_service.human_name
+
+ @property
+ def assistant_name(self):
+ return self.memory_service.assistant_name
+
+ @property
+ def generation_model(self) -> BaseModel:
+ """
+ Property to get the generation model. If the model is set as a string, it will be resolved from the global
+ context's model dictionary.
+
+ Raises:
+ ValueError: If the declaration of generation model is not found in the model dictionary of context .
+
+ Returns:
+ BaseModel: An actual generation model instance.
+ """
+ if isinstance(self._generation_model, str):
+ if self._generation_model not in self.context.model_dict:
+ raise ValueError(f"Missing declaration of generation model in yaml config: {self._generation_model}")
+ self._generation_model = self.context.model_dict[self._generation_model]
+ return self._generation_model
+
+ def iter_response(self,
+ remember_response: bool,
+ resp: ModelResponseGen,
+ memories: str,
+ query_message: Message) -> ModelResponseGen:
+
+ model_response: ModelResponse | None = None
+ for model_response in resp:
+ yield model_response
+
+ if remember_response:
+ if model_response and model_response.message:
+ model_response.message.role_name = self.assistant_name
+ model_response.meta_data[MEMORIES] = memories
+ self.memory_service.add_messages_pair([query_message, model_response.message])
+ else:
+ self.logger.warning("model_response or model_response.message is empty!")
+
+ def chat_with_memory(self,
+ query: str,
+ role_name: Optional[str] = None,
+ system_prompt: Optional[str] = None,
+ memory_prompt: Optional[str] = None,
+ temporary_memories: Optional[str] = None,
+ history_message_strategy: Literal["auto", None] | int = "auto",
+ remember_response: bool = True,
+ **kwargs):
+
+ chat_messages: List[Message] = []
+
+ # prepare query message
+ if not role_name:
+ role_name = self.human_name
+ query_message = Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query)
+
+ # To retrieve memory, prepare the query timestamp and role name by adding query_message.
+ memories: str = self.memory_service.retrieve_memory(query=query_message.content,
+ role_name=query_message.role_name,
+ timestamp=query_message.time_created)
+
+ # format system_message with memories
+ system_prompt_list = []
+ if system_prompt:
+ system_prompt_list.append(system_prompt)
+ else:
+ dt_handler = DatetimeHandler()
+ date_time = dt_handler.datetime_format("%Y-%m-%d %H:%M:%S")
+ weekday = dt_handler.get_dt_info_dict(self.context.language)["weekday"]
+ system_prompt_list.append(self.prompt_handler.system_prompt.format(date_time=f"{date_time} {weekday}"))
+
+ if memories:
+ # add memory prompt
+ if memory_prompt:
+ system_prompt_list.append(memory_prompt)
+ else:
+ system_prompt_list.append(self.prompt_handler.memory_prompt)
+
+ if self.human_name != DEFAULT_HUMAN_NAME[self.context.language]:
+ system_prompt_list.append(USER_NAME_EXPRESSION[self.context.language].format(name=self.human_name))
+ system_prompt_list.append(memories)
+
+ if temporary_memories:
+ system_prompt_list.extend(temporary_memories)
+
+ system_prompt_join = "\n".join([x.strip() for x in system_prompt_list])
+ system_message = Message(role=MessageRoleEnum.SYSTEM.value, content=system_prompt_join)
+ chat_messages.append(system_message)
+
+ # Include past conversation history in the message list
+ if history_message_strategy:
+ history_messages = []
+
+ if history_message_strategy == "auto":
+ history_messages = self.memory_service.read_message()
+
+ elif isinstance(history_message_strategy, int):
+ history_messages = self.memory_service.get_chat_messages_scatter(history_message_strategy)
+
+ if history_messages:
+ assert isinstance(history_messages[0], Message)
+ chat_messages.extend(history_messages)
+
+ # Append the current user's message to the conversation context
+ chat_messages.append(query_message)
+ self.logger.info(f"chat_messages={chat_messages}")
+
+ resp = self.generation_model.call(messages=chat_messages, stream=self.stream, **self.generation_model_kwargs)
+ if self.stream:
+ return self.iter_response(remember_response, resp, memories, query_message)
+
+ else:
+ model_response: ModelResponse = resp
+ if remember_response:
+ if model_response and model_response.message:
+ model_response.message.role_name = self.assistant_name
+ model_response.meta_data[MEMORIES] = memories
+ self.memory_service.add_messages_pair([query_message, model_response.message])
+ else:
+ self.logger.warning("model_response or model_response.message is empty!")
+ return model_response
diff --git a/memoryscope/memoryscope/core/chat/base_memory_chat.py b/memoryscope/memoryscope/core/chat/base_memory_chat.py
new file mode 100644
index 00000000..9558c92d
--- /dev/null
+++ b/memoryscope/memoryscope/core/chat/base_memory_chat.py
@@ -0,0 +1,75 @@
+from abc import ABCMeta, abstractmethod
+from typing import Optional, Literal
+
+from memoryscope.core.service.base_memory_service import BaseMemoryService
+from memoryscope.core.utils.logger import Logger
+
+
+class BaseMemoryChat(metaclass=ABCMeta):
+ """
+ An abstract base class representing a chat system integrated with memory services.
+ It outlines the method to initiate a chat session leveraging memory data, which concrete subclasses must implement.
+ """
+
+ def __init__(self, **kwargs):
+ self.kwargs: dict = kwargs
+ self.logger = Logger.get_logger()
+
+ @property
+ def memory_service(self) -> BaseMemoryService:
+ """
+ Abstract property to access the memory service.
+
+ Raises:
+ NotImplementedError: This method should be implemented in a subclass.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def chat_with_memory(self,
+ query: str,
+ role_name: Optional[str] = None,
+ system_prompt: Optional[str] = None,
+ memory_prompt: Optional[str] = None,
+ temporary_memories: Optional[str] = None,
+ history_message_strategy: Literal["auto", None] | int = "auto",
+ remember_response: bool = True,
+ **kwargs):
+ """
+ The core function that carries out conversation with memory accepts user queries through query and returns the
+ conversation results through model_response. The retrieved memories are stored in the memories within meta_data.
+ Args:
+ query (str): User's query, includes the user's question.
+ role_name (str, optional): User's role name.
+ system_prompt (str, optional): System prompt. Defaults to the system_prompt in "memory_chat_prompt.yaml".
+ memory_prompt (str, optional): Memory prompt, It takes effect when there is a memory and will be placed in
+ front of the retrieved memory. Defaults to the memory_prompt in "memory_chat_prompt.yaml".
+ temporary_memories (str, optional): Manually added user memory in this function.
+ history_message_strategy ("auto", None, int):
+ - If it is set to "auto", the history messages in the conversation will retain those that have not
+ yet been summarized. Default to "auto".
+ - If it is set to None, no conversation history will be saved.
+ - If it is set to an integer value "n", recent "n" message-pair[user, assistant] will be retained.
+ remember_response (bool, optional): Flag indicating whether to save the AI's response to memory.
+ Defaults to False.
+ Returns:
+ - ModelResponse: In non-streaming mode, returns a complete AI response.
+ - ModelResponseGen: In streaming mode, returns a generator yielding AI response parts.
+ - Memories: To obtain the memory by invoking the method of model_response.meta_data[MEMORIES]
+ """
+ raise NotImplementedError
+
+ def start_backend_service(self, **kwargs):
+ self.memory_service.start_backend_service(**kwargs)
+
+ def run_service_operation(self, name: str, role_name: Optional[str] = None, **kwargs):
+ return self.memory_service.run_operation(name, role_name=role_name, **kwargs)
+
+ def run(self):
+ """
+ Abstract method to run the chat system.
+
+ This method should contain the logic to initiate and manage the chat process,
+ utilizing the memory service as needed. It must be implemented by subclasses.
+ """
+ pass
diff --git a/memoryscope/memoryscope/core/chat/cli_memory_chat.py b/memoryscope/memoryscope/core/chat/cli_memory_chat.py
new file mode 100644
index 00000000..c3e93b78
--- /dev/null
+++ b/memoryscope/memoryscope/core/chat/cli_memory_chat.py
@@ -0,0 +1,209 @@
+import os
+import time
+from typing import Optional, Literal
+
+import questionary
+
+from memoryscope.core.chat.api_memory_chat import ApiMemoryChat
+from memoryscope.core.utils.tool_functions import char_logo
+
+
+class CliMemoryChat(ApiMemoryChat):
+ """
+ Command-line interface for chatting with an AI that integrates memory functionality.
+ Allows users to interact, manage chat history, adjust streaming settings, and view commands' help.
+ """
+ USER_COMMANDS = {
+ "exit": "Exit the CLI.",
+ "clear": "Clear the command history.",
+ "help": "Display available CLI commands and their descriptions.",
+ "stream": "Toggle between getting streamed responses from the model."
+ }
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self._logo = char_logo("MemoryScope")
+
+ def print_logo(self):
+ """
+ Prints the logo of the CLI application to the console.
+
+ The logo is composed of multiple lines, which are iterated through
+ and printed one by one to provide a visual identity for the chat interface.
+ """
+ for line in self._logo:
+ print(line)
+
+ def chat_with_memory(self,
+ query: str,
+ role_name: Optional[str] = None,
+ system_prompt: Optional[str] = None,
+ memory_prompt: Optional[str] = None,
+ temporary_memories: Optional[str] = None,
+ history_message_strategy: Literal["auto", None] | int = "auto",
+ remember_response: bool = True,
+ **kwargs):
+ resp = super().chat_with_memory(query=query,
+ role_name=role_name,
+ system_prompt=system_prompt,
+ memory_prompt=memory_prompt,
+ temporary_memories=temporary_memories,
+ history_message_strategy=history_message_strategy,
+ remember_response=remember_response,
+ **kwargs)
+
+ if self.stream:
+ for _resp in resp:
+ questionary.print(_resp.delta, end="")
+ questionary.print("")
+ else:
+ questionary.print(resp.message.content)
+
+ @staticmethod
+ def parse_query_command(query: str):
+ """
+ Parses the user's input query command, separating it into the command and its associated keyword arguments.
+
+ Args:
+ query (str): The raw input string from the user which includes the command and its arguments.
+
+ Returns:
+ tuple: A tuple containing the command (str) as the first element and a dictionary (kwargs) of keyword
+ arguments as the second element.
+ """
+ query_split = query.lstrip("/").lower().split(" ") # Split and preprocess the input command
+ command = query_split[0] # Extract the command
+ args = query_split[1:] # Extract the arguments following the command
+ kwargs = {} # Initialize dictionary to hold keyword arguments
+
+ for arg in args:
+ # Skip if no arguments exist (unnecessary check due to prior assignment, but retained as per original)
+ if not args:
+ continue
+ arg_split = arg.split("=") # Split argument into key-value pair
+ if len(arg_split) >= 2: # Ensure there's both a key and value
+ k = arg_split[0] # Extract key
+ v = arg_split[1] # Extract value
+ if k and v: # Only add to kwargs if both key and value are non-empty
+ kwargs[k] = v
+
+ return command, kwargs # Return the parsed command and keyword arguments
+
+ def process_commands(self, query: str) -> bool:
+ """
+ Parses and executes commands from user input in the CLI chat interface.
+ Supports operations like exiting, clearing screen, showing help, toggling stream mode,
+ executing predefined memory operations, and handling unknown commands.
+
+ Args:
+ query (str): The user's input command string.
+
+ Returns:
+ bool: Indicates whether to continue running the CLI after processing the command.
+ """
+ continue_run = True
+ command, kwargs = self.parse_query_command(query)
+
+ # Print prompt for AI's response
+ questionary.print("> ", end="", style="fg:yellow")
+ questionary.print(f"{self.assistant_name}: ", end="", style="bold")
+
+ if command == "exit":
+ self.memory_service.stop_backend_service()
+ continue_run = False
+
+ elif command == "clear":
+ os.system("clear")
+
+ elif command == "help":
+ questionary.print("CLI commands", "bold")
+ for cmd, desc in self.USER_COMMANDS.items():
+ questionary.print(text=f" /{cmd}:", style="bold")
+ questionary.print(text=f" {desc}")
+
+ elif command == "stream":
+ self.stream = not self.stream
+ questionary.print(f"set stream: {self.stream}")
+
+ elif command in self.memory_service.op_description_dict:
+ refresh_time = kwargs.pop("refresh_time", "")
+ if refresh_time and refresh_time.isdigit():
+ refresh_time = int(refresh_time)
+ self.memory_service.stop_backend_service()
+ while True:
+ result = self.memory_service.run_operation(name=command, **kwargs)
+ if os.name == 'nt':
+ os.system('cls')
+ else:
+ os.system('clear')
+ self.print_logo()
+ if result:
+ if isinstance(result, list):
+ result = "\n".join([str(x) for x in result])
+ questionary.print(result)
+ else:
+ questionary.print(f"command={command} result is empty! kwargs={kwargs}")
+ time.sleep(refresh_time)
+
+ else:
+ result = self.memory_service.run_operation(name=command, **kwargs)
+ if result:
+ if isinstance(result, list):
+ result = "\n".join([str(x) for x in result])
+ questionary.print(result)
+ else:
+ questionary.print(f"command={command} result is empty! kwargs={kwargs}")
+
+ else:
+ questionary.print(f"Unknown command={command} received.")
+
+ return continue_run
+
+ def run(self):
+ """
+ Runs the CLI chat loop, which handles user input, processes commands,
+ communicates with the AI model, manages conversation memory, and controls
+ the chat session including streaming responses, command execution, and error handling.
+
+ The loop continues until the user explicitly chooses to exit.
+ """
+ self.print_logo()
+ self.USER_COMMANDS.update(self.memory_service.op_description_dict)
+
+ while True:
+ try:
+ query = questionary.text(message=f"{self.human_name}:", multiline=False, qmark=">").unsafe_ask()
+ if not query:
+ continue
+
+ query: str = query.strip()
+
+ # Handle special commands prefixed with '/'
+ if query.startswith("/"):
+ if self.process_commands(query=query):
+ continue
+ else:
+ break
+
+ # Print prompt for AI's response
+ questionary.print("> ", end="", style="fg:yellow")
+ questionary.print(f"{self.assistant_name}: ", end="", style="bold")
+
+ # Fetch and display AI's response
+ self.start_backend_service()
+ self.chat_with_memory(query=query)
+
+ except KeyboardInterrupt:
+ # Handle user interruption and confirm exit
+ questionary.print("User interrupt occurred.")
+ is_exit = questionary.confirm("Continue exit?").unsafe_ask()
+ if is_exit:
+ self.memory_service.stop_backend_service()
+ break
+
+ except Exception as e:
+ # Log and handle any unanticipated exceptions
+ import traceback
+ traceback.print_exc()
+ self.logger.exception(f"An exception occurred when running cli memory chat. args={e.args}.")
+ continue
diff --git a/memoryscope/memoryscope/core/chat/memory_chat_prompt.yaml b/memoryscope/memoryscope/core/chat/memory_chat_prompt.yaml
new file mode 100644
index 00000000..5bfdbf40
--- /dev/null
+++ b/memoryscope/memoryscope/core/chat/memory_chat_prompt.yaml
@@ -0,0 +1,11 @@
+system_prompt:
+ cn: |
+ 你是一个名为MemoryScope的智能助理,请用中文简洁地回答用户问题。当前时间是{date_time}。
+ en: |
+ You are a helpful assistant named MemoryScope, please answer questions concisely in English. The current time is {date_time}.
+
+memory_prompt:
+ cn: |
+ 在回答用户问题时,请尽量忘记大部分不相关的信息。只有当信息与用户问题或对话内容非常相关时,才记住这些信息并加以使用。请确保你的回答简洁、准确,并聚焦于用户问题或对话主题。信息:
+ en: |
+ When responding to user questions, please try to forget most of the irrelevant information. Only remember and use the information if it is highly relevant to the current question or conversation. Ensure that your answers are concise, accurate, and focused on the user's current question or the topic of discussion. Information:
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/config/__init__.py b/memoryscope/memoryscope/core/config/__init__.py
new file mode 100644
index 00000000..21bb27f2
--- /dev/null
+++ b/memoryscope/memoryscope/core/config/__init__.py
@@ -0,0 +1,7 @@
+from .arguments import Arguments
+from .config_manager import ConfigManager
+
+__all__ = [
+ "Arguments",
+ "ConfigManager",
+]
diff --git a/memoryscope/memoryscope/core/config/arguments.py b/memoryscope/memoryscope/core/config/arguments.py
new file mode 100644
index 00000000..d4ecab1b
--- /dev/null
+++ b/memoryscope/memoryscope/core/config/arguments.py
@@ -0,0 +1,81 @@
+from dataclasses import dataclass, field
+from typing import Literal, Dict
+
+
+@dataclass
+class Arguments(object):
+ language: Literal["cn", "en"] = field(default="cn", metadata={"help": "support en & cn now"})
+
+ thread_pool_max_workers: int = field(default=5, metadata={"help": "thread pool max workers"})
+
+ memory_chat_class: str = field(default="cli_memory_chat", metadata={
+ "help": "cli_memory_chat(Command-line interaction), api_memory_chat(API interface interaction), etc."})
+
+ chat_stream: bool | None = field(default=None, metadata={
+ "help": "In the case of cli_memory_chat, stream mode is recommended. For api_memory_chat mode, "
+ "please use non-stream. If set to None, the value will be automatically determined."})
+
+ human_name: str = field(default="user", metadata={"help": "Human user's name"})
+
+ assistant_name: str = field(default="AI", metadata={"help": "assistant' name"})
+
+ consolidate_memory_interval_time: int | None = field(default=1, metadata={
+ "help": "Memory backend service: If you feel that the token consumption is relatively high, "
+ "please increase the time interval. When set to None, the value will not be updated."})
+
+ reflect_and_reconsolidate_interval_time: int | None = field(default=15, metadata={
+ "help": "Memory backend service: If you feel that the token consumption is relatively high, "
+ "please increase the time interval. When set to None, the value will not be updated."})
+
+ worker_params: Dict[str, dict] = field(default_factory=lambda: {}, metadata={
+ "help": "dict format: worker_name -> param_key -> param_value"})
+
+ generation_backend: str = field(default="dashscope_generation", metadata={
+ "help": "global generation backend: openai_generation, dashscope_generation, etc."})
+
+ generation_model: str = field(default="qwen-max", metadata={
+ "help": "global generation model: gpt-4o, gpt-4o-mini, gpt-4-turbo, qwen-max, etc."})
+
+ generation_params: dict = field(default_factory=lambda: {}, metadata={
+ "help": "global generation params: max_tokens, top_p, temperature, etc."})
+
+ embedding_backend: str = field(default="dashscope_generation", metadata={
+ "help": "global embedding backend: openai_embedding, dashscope_embedding, etc."})
+
+ embedding_model: str = field(default="text-embedding-v2", metadata={
+ "help": "global embedding model: text-embedding-3-large, text-embedding-3-small, text-embedding-ada-002, "
+ "text-embedding-v2, etc."})
+
+ embedding_params: dict = field(default_factory=lambda: {})
+
+ rank_backend: str = field(default="dashscope_rank", metadata={"help": "global rank backend: dashscope_rank, etc."})
+
+ rank_model: str = field(default="gte-rerank", metadata={"help": "global rank model: gte-rerank, etc."})
+
+ rank_params: dict = field(default_factory=lambda: {})
+
+ es_index_name: str = field(default="memory_index")
+
+ es_url: str = field(default="http://localhost:9200")
+
+ retrieve_mode: str = field(default="dense", metadata={
+ "help": "retrieve_mode: dense, sparse(not implemented), hybrid(not implemented)"})
+
+ enable_ranker: bool = field(default=False, metadata={
+ "help": "If a semantic ranking model is not available, MemoryScope will use cosine similarity scoring as a "
+ "substitute. However, the ranking effectiveness will be somewhat compromised.",
+ "map_yaml": "global->enable_ranker"})
+
+ enable_today_contra_repeat: bool = field(default=True, metadata={
+ "help": "Whether enable conflict resolution and deduplication for the day? "
+ "Note that enabling this will increase token consumption.",
+ "map_yaml": "global->enable_today_contra_repeat"})
+
+ enable_long_contra_repeat: bool = field(default=False, metadata={
+ "help": "Whether to enable long-term conflict resolution and deduplication. "
+ "Note that enabling this will increase token consumption.",
+ "map_yaml": "global->enable_long_contra_repeat"})
+
+ output_memory_max_count: int = field(default=20, metadata={
+ "help": "The maximum number of memories retrieved during memory recall.",
+ "map_yaml": "global->output_memory_max_count"})
diff --git a/memoryscope/memoryscope/core/config/config_manager.py b/memoryscope/memoryscope/core/config/config_manager.py
new file mode 100644
index 00000000..829b7265
--- /dev/null
+++ b/memoryscope/memoryscope/core/config/config_manager.py
@@ -0,0 +1,197 @@
+import json
+import os
+from dataclasses import fields
+from datetime import datetime
+from pathlib import Path
+from typing import Optional, Literal
+
+import yaml
+
+from memoryscope.core.config.arguments import Arguments
+from memoryscope.core.utils.logger import Logger
+
+
+class ConfigManager(object):
+
+ def __init__(self,
+ config_path: Optional[str] = None,
+ arguments: Optional[Arguments] = None,
+ demo_config_name: str = "demo_config_zh.yaml",
+ **kwargs):
+ self.config: dict = {}
+ self.kwargs = kwargs
+ self.logger = Logger.get_logger("memoryscope")
+
+ if not (config_path or kwargs or arguments):
+ raise RuntimeError("can not init config manager without kwargs or --config_path!")
+
+ if config_path:
+ self.read_config(config_path)
+ else:
+ self.read_config((Path(__file__).parent / demo_config_name).__str__())
+
+ kwargs = {k: v for k, v in kwargs.items() if k in [x.name for x in fields(Arguments)]}
+ kwargs_padding = {x.name: None for x in fields(Arguments) if x.name not in kwargs}
+ kwargs.update(kwargs_padding)
+
+ # (high) when there are environment variables, read them and merge into kwargs
+ kwargs_from_env = {x.name:os.environ.get(x.name, None) for x in fields(Arguments) if os.environ.get(x.name, None) is not None}
+ kwargs.update(kwargs_from_env)
+
+ # generate argument dataclass
+ if not arguments:
+ arguments = Arguments(**kwargs)
+ else:
+ # (highest) when arguments is passed into the memoryscope
+ arguments = arguments
+
+ self.update_config_by_arguments(arguments)
+ self.logger.info("\n" + self.dump_config())
+
+ def read_config(self, config_path: str):
+ if config_path.endswith(".yaml"):
+ with open(config_path) as f:
+ self.config = yaml.load(f, yaml.FullLoader)
+
+ elif config_path.endswith(".json"):
+ with open(config_path) as f:
+ self.config = json.load(f)
+
+ @staticmethod
+ def update_ignore_none(config, new_config_dict):
+ update_dict = {k:v for k, v in new_config_dict.items() if v is not None}
+ config.update(update_dict)
+ return
+
+ @staticmethod
+ def update_global_by_arguments(config: dict, arguments: Arguments):
+ ConfigManager.update_ignore_none(
+ config,
+ {
+ "language": arguments.language,
+ "thread_pool_max_workers": arguments.thread_pool_max_workers,
+ "enable_ranker": arguments.enable_ranker,
+ "enable_today_contra_repeat": arguments.enable_today_contra_repeat,
+ "enable_long_contra_repeat": arguments.enable_long_contra_repeat,
+ "output_memory_max_count": arguments.output_memory_max_count,
+ }
+ )
+
+ @staticmethod
+ def update_memory_chat_by_arguments(config: dict, arguments: Arguments):
+ if arguments.memory_chat_class is not None:
+ memory_chat_class_split = config["class"].split(".")
+ stream = arguments.chat_stream
+ if stream is None:
+ stream = arguments.memory_chat_class in ["cli_memory_chat", ]
+ config.update(
+ {
+ "class": ".".join(memory_chat_class_split[:-1] + [arguments.memory_chat_class]),
+ "stream": stream,
+ }
+ )
+
+ @staticmethod
+ def update_memory_service_by_arguments(config: dict, arguments: Arguments):
+ ConfigManager.update_ignore_none(config, {
+ "human_name": arguments.human_name,
+ "assistant_name": arguments.assistant_name,
+ })
+ if arguments.consolidate_memory_interval_time is not None:
+ config["memory_operations"]["consolidate_memory"]["interval_time"] = \
+ arguments.consolidate_memory_interval_time
+
+ if arguments.reflect_and_reconsolidate_interval_time is not None:
+ config["memory_operations"]["reflect_and_reconsolidate"]["interval_time"] = \
+ arguments.reflect_and_reconsolidate_interval_time
+
+ @staticmethod
+ def update_worker_by_arguments(config: dict, arguments: Arguments):
+ if arguments.worker_params is not None:
+ for worker_name, kv_dict in arguments.worker_params.items():
+ if worker_name not in config:
+ continue
+ config[worker_name].update(kv_dict)
+
+ @staticmethod
+ def update_model_by_arguments(config: dict, arguments: Arguments):
+ ConfigManager.update_ignore_none(config["generation_model"], {
+ "module_name": arguments.generation_backend,
+ "model_name": arguments.generation_model,
+ })
+ if isinstance(arguments.generation_params, dict):
+ ConfigManager.update_ignore_none(config["generation_model"], {
+ **arguments.generation_params,
+ })
+
+ ConfigManager.update_ignore_none(config["embedding_model"], {
+ "module_name": arguments.embedding_backend,
+ "model_name": arguments.embedding_model,
+ })
+ if isinstance(arguments.embedding_params, dict):
+ ConfigManager.update_ignore_none(config["embedding_model"], {
+ **arguments.embedding_params,
+ })
+
+ ConfigManager.update_ignore_none(config["rank_model"], {
+ "module_name": arguments.rank_backend,
+ "model_name": arguments.rank_model,
+ })
+ if isinstance(arguments.rank_params, dict):
+ ConfigManager.update_ignore_none(config["rank_model"], {
+ **arguments.rank_params,
+ })
+
+ @staticmethod
+ def update_memory_store_by_arguments(config: dict, arguments: Arguments):
+ ConfigManager.update_ignore_none(config, {
+ "index_name": arguments.es_index_name,
+ "es_url": arguments.es_url,
+ "retrieve_mode": arguments.retrieve_mode}
+ )
+
+ def update_config_by_arguments(self, arguments: Arguments):
+ # prepare global
+ self.update_global_by_arguments(self.config["global"], arguments)
+
+ # prepare memory chat
+ memory_chat_conf_dict = self.config["memory_chat"]
+ memory_chat_config = list(memory_chat_conf_dict.values())[0]
+ self.update_memory_chat_by_arguments(memory_chat_config, arguments)
+
+ # prepare memory service
+ memory_service_conf_dict = self.config["memory_service"]
+ memory_service_config = list(memory_service_conf_dict.values())[0]
+ self.update_memory_service_by_arguments(memory_service_config, arguments)
+
+ # prepare worker
+ self.update_worker_by_arguments(self.config["worker"], arguments)
+
+ # prepare model
+ self.update_model_by_arguments(self.config["model"], arguments)
+
+ # prepare memory store
+ self.update_memory_store_by_arguments(self.config["memory_store"], arguments)
+
+ def add_node_object(self, node: str, name: str, config: dict):
+ self.config[node][name] = config
+
+ def pop_node_object(self, node: str, name: str):
+ return self.config[node].pop(name, None)
+
+ def clear_node_all(self, node: str):
+ self.config[node].clear()
+
+ def dump_config(self, file_type: Literal["json", "yaml"] = "yaml", file_path: Optional[str] = None) -> str:
+ if file_type == "json":
+ content = json.dumps(self.config, indent=2, ensure_ascii=False)
+ elif file_type == "yaml":
+ content = yaml.dump(self.config, indent=2, allow_unicode=True)
+ else:
+ raise NotImplementedError
+
+ if file_path:
+ with open(file_path, "w") as f:
+ f.write(content)
+
+ return content
diff --git a/memoryscope/memoryscope/core/config/demo_config.yaml b/memoryscope/memoryscope/core/config/demo_config.yaml
new file mode 100644
index 00000000..2e54eac8
--- /dev/null
+++ b/memoryscope/memoryscope/core/config/demo_config.yaml
@@ -0,0 +1,179 @@
+global:
+ language: en
+ thread_pool_max_workers: 5
+ enable_ranker: false
+ enable_today_contra_repeat: true
+ enable_long_contra_repeat: false
+ output_memory_max_count: 20
+
+memory_chat:
+ cli_memory_chat:
+ class: core.chat.cli_memory_chat
+ memory_service: memoryscope_service
+ generation_model: generation_model
+ stream: true
+
+memory_service:
+ memoryscope_service:
+ class: core.service.memory_scope_service
+ human_name: user
+ assistant_name: AI
+ memory_operations:
+ read_message:
+ class: core.operation.frontend_operation
+ workflow: read_message
+ description: "read short memory"
+
+ retrieve_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,[extract_time|retrieve_obs_ins,semantic_rank],fuse_rerank
+ description: "retrieve long-term memory"
+
+ list_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_top_memory,print_memory
+ description: "read all long-term memory of the user, use `refresh_time=5` to refresh screen every 5 seconds."
+
+ delete_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_memory
+ description: "delete a single long-term memory"
+
+ delete_all:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_all
+ description: "delete all long-term memory"
+
+ add_memory:
+ class: core.operation.frontend_operation
+ workflow: add_memory
+ description: "add a single observation"
+
+ consolidate_memory:
+ class: core.operation.consolidate_memory_op
+ workflow: info_filter,[get_observation|get_observation_with_time|load_today_memory],contra_repeat,store_memory
+ description: "summary user's observation memory, run backend."
+ interval_time: 1
+
+ reflect_and_reconsolidate:
+ class: core.operation.backend_operation
+ workflow: load_obs_and_insight,get_reflection_subject,update_insight,long_contra_repeat,store_memory
+ description: "summary user's insight memory, run backend."
+ interval_time: 15
+
+worker:
+ dummy:
+ class: core.worker.dummy_worker
+ generation_model: generation_model
+ embedding_model: embedding_model
+ rank_model: rank_model
+ read_message:
+ class: core.worker.frontend.read_message_worker
+ set_query:
+ class: core.worker.frontend.set_query_worker
+ retrieve_obs_ins:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ extract_time:
+ class: core.worker.frontend.extract_time_worker
+ generation_model: generation_model
+ semantic_rank:
+ class: core.worker.frontend.semantic_rank_worker
+ rank_model: rank_model
+ fuse_rerank:
+ class: core.worker.frontend.fuse_rerank_worker
+ fuse_score_threshold: 0.01
+ fuse_ratio_dict:
+ conversation: 0.5
+ observation: 1
+ obs_customized: 1.2
+ insight: 2.0
+ fuse_time_ratio: 2.0
+ retrieve_top_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ retrieve_expired_top_k: 100
+ print_memory:
+ class: core.worker.frontend.print_memory_worker
+ retrieve_all_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 1000
+ retrieve_ins_top_k: 1000
+ retrieve_expired_top_k: 1000
+ delete_memory:
+ class: core.worker.backend.update_memory_worker
+ method: delete_memory
+ delete_all:
+ class: core.worker.backend.update_memory_worker
+ method: delete_all
+ add_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_query
+ info_filter:
+ class: core.worker.backend.info_filter_worker
+ generation_model: generation_model
+ load_today_memory:
+ class: core.worker.backend.load_memory_worker
+ retrieve_today_top_k: 100
+ get_observation:
+ class: core.worker.backend.get_observation_worker
+ generation_model: generation_model
+ get_observation_with_time:
+ class: core.worker.backend.get_observation_with_time_worker
+ generation_model: generation_model
+ contra_repeat:
+ class: core.worker.backend.contra_repeat_worker
+ generation_model: generation_model
+ store_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_memory_key
+ memory_key: all
+ load_obs_and_insight:
+ class: core.worker.backend.load_memory_worker
+ retrieve_not_reflected_top_k: 100
+ retrieve_not_updated_top_k: 100
+ retrieve_insight_top_k: 100
+ get_reflection_subject:
+ class: core.worker.backend.get_reflection_subject_worker
+ generation_model: generation_model
+ reflect_obs_cnt_threshold: 6
+ update_insight:
+ class: core.worker.backend.update_insight_worker
+ generation_model: generation_model
+ rank_model: rank_model
+ embedding_model: embedding_model
+ update_insight_threshold: 0.01
+ enable_parallel: false
+ long_contra_repeat:
+ class: core.worker.backend.long_contra_repeat_worker
+ generation_model: generation_model
+ long_contra_repeat_threshold: 0.5
+
+model:
+ generation_model:
+ class: core.models.llama_index_generation_model
+ module_name: openai_generation
+ model_name: gpt-4o
+ max_tokens: 2000
+ temperature: 0.01
+ embedding_model:
+ class: core.models.llama_index_embedding_model
+ module_name: openai_embedding
+ model_name: text-embedding-3-small
+ rank_model:
+ class: core.models.llama_index_rank_model
+ module_name: dashscope_rank
+ model_name: gte-rerank
+ top_n: 500
+
+memory_store:
+ class: core.storage.llama_index_es_memory_store
+ embedding_model: embedding_model
+ index_name: memory_index
+ es_url: http://localhost:9200
+ retrieve_mode: dense
+
+monitor:
+ class: core.storage.dummy_monitor
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/config/demo_config_zh.yaml b/memoryscope/memoryscope/core/config/demo_config_zh.yaml
new file mode 100644
index 00000000..3dc49b8c
--- /dev/null
+++ b/memoryscope/memoryscope/core/config/demo_config_zh.yaml
@@ -0,0 +1,179 @@
+global:
+ language: cn
+ thread_pool_max_workers: 5
+ enable_ranker: true
+ enable_today_contra_repeat: true
+ enable_long_contra_repeat: false
+ output_memory_max_count: 20
+
+memory_chat:
+ cli_memory_chat:
+ class: core.chat.cli_memory_chat
+ memory_service: memoryscope_service
+ generation_model: generation_model
+ stream: true
+
+memory_service:
+ memoryscope_service:
+ class: core.service.memory_scope_service
+ human_name: 用户
+ assistant_name: AI
+ memory_operations:
+ read_message:
+ class: core.operation.frontend_operation
+ workflow: read_message
+ description: "read short memory"
+
+ retrieve_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,[extract_time|retrieve_obs_ins,semantic_rank],fuse_rerank
+ description: "retrieve long-term memory"
+
+ list_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_top_memory,print_memory
+ description: "read all long-term memory of the user, use `refresh_time=5` to refresh screen every 5 seconds."
+
+ delete_memory:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_memory
+ description: "delete a single long-term memory"
+
+ delete_all:
+ class: core.operation.frontend_operation
+ workflow: set_query,retrieve_all_memory,delete_all
+ description: "delete all long-term memory"
+
+ add_memory:
+ class: core.operation.frontend_operation
+ workflow: add_memory
+ description: "add a single observation"
+
+ consolidate_memory:
+ class: core.operation.consolidate_memory_op
+ workflow: info_filter,[get_observation|get_observation_with_time|load_today_memory],contra_repeat,store_memory
+ description: "summary user's observation memory, run backend."
+ interval_time: 1
+
+ reflect_and_reconsolidate:
+ class: core.operation.backend_operation
+ workflow: load_obs_and_insight,get_reflection_subject,update_insight,long_contra_repeat,store_memory
+ description: "summary user's insight memory, run backend."
+ interval_time: 15
+
+worker:
+ dummy:
+ class: core.worker.dummy_worker
+ generation_model: generation_model
+ embedding_model: embedding_model
+ rank_model: rank_model
+ read_message:
+ class: core.worker.frontend.read_message_worker
+ set_query:
+ class: core.worker.frontend.set_query_worker
+ retrieve_obs_ins:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ extract_time:
+ class: core.worker.frontend.extract_time_worker
+ generation_model: generation_model
+ semantic_rank:
+ class: core.worker.frontend.semantic_rank_worker
+ rank_model: rank_model
+ fuse_rerank:
+ class: core.worker.frontend.fuse_rerank_worker
+ fuse_score_threshold: 0.01
+ fuse_ratio_dict:
+ conversation: 0.5
+ observation: 1
+ obs_customized: 1.2
+ insight: 2.0
+ fuse_time_ratio: 2.0
+ retrieve_top_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 100
+ retrieve_ins_top_k: 100
+ retrieve_expired_top_k: 100
+ print_memory:
+ class: core.worker.frontend.print_memory_worker
+ retrieve_all_memory:
+ class: core.worker.frontend.retrieve_memory_worker
+ retrieve_obs_top_k: 1000
+ retrieve_ins_top_k: 1000
+ retrieve_expired_top_k: 1000
+ delete_memory:
+ class: core.worker.backend.update_memory_worker
+ method: delete_memory
+ delete_all:
+ class: core.worker.backend.update_memory_worker
+ method: delete_all
+ add_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_query
+ info_filter:
+ class: core.worker.backend.info_filter_worker
+ generation_model: generation_model
+ load_today_memory:
+ class: core.worker.backend.load_memory_worker
+ retrieve_today_top_k: 100
+ get_observation:
+ class: core.worker.backend.get_observation_worker
+ generation_model: generation_model
+ get_observation_with_time:
+ class: core.worker.backend.get_observation_with_time_worker
+ generation_model: generation_model
+ contra_repeat:
+ class: core.worker.backend.contra_repeat_worker
+ generation_model: generation_model
+ store_memory:
+ class: core.worker.backend.update_memory_worker
+ method: from_memory_key
+ memory_key: all
+ load_obs_and_insight:
+ class: core.worker.backend.load_memory_worker
+ retrieve_not_reflected_top_k: 100
+ retrieve_not_updated_top_k: 100
+ retrieve_insight_top_k: 100
+ get_reflection_subject:
+ class: core.worker.backend.get_reflection_subject_worker
+ generation_model: generation_model
+ reflect_obs_cnt_threshold: 6
+ update_insight:
+ class: core.worker.backend.update_insight_worker
+ generation_model: generation_model
+ rank_model: rank_model
+ embedding_model: embedding_model
+ update_insight_threshold: 0.01
+ enable_parallel: false
+ long_contra_repeat:
+ class: core.worker.backend.long_contra_repeat_worker
+ generation_model: generation_model
+ long_contra_repeat_threshold: 0.5
+
+model:
+ generation_model:
+ class: core.models.llama_index_generation_model
+ module_name: dashscope_generation
+ model_name: qwen-max
+ max_tokens: 2000
+ temperature: 0.01
+ embedding_model:
+ class: core.models.llama_index_embedding_model
+ module_name: dashscope_embedding
+ model_name: text-embedding-v2
+ rank_model:
+ class: core.models.llama_index_rank_model
+ module_name: dashscope_rank
+ model_name: gte-rerank
+ top_n: 500
+
+memory_store:
+ class: core.storage.llama_index_es_memory_store
+ embedding_model: embedding_model
+ index_name: memory_index
+ es_url: http://localhost:9200
+ retrieve_mode: dense
+
+monitor:
+ class: core.storage.dummy_monitor
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/memoryscope.py b/memoryscope/memoryscope/core/memoryscope.py
new file mode 100644
index 00000000..12381634
--- /dev/null
+++ b/memoryscope/memoryscope/core/memoryscope.py
@@ -0,0 +1,117 @@
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime
+
+from memoryscope.core.chat.base_memory_chat import BaseMemoryChat
+from memoryscope.core.config.config_manager import ConfigManager
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.service.base_memory_service import BaseMemoryService
+from memoryscope.core.utils.tool_functions import init_instance_by_config
+from memoryscope.enumeration.language_enum import LanguageEnum
+from memoryscope.enumeration.model_enum import ModelEnum
+
+
+class MemoryScope(ConfigManager):
+
+ def __init__(self, **kwargs):
+ self._context: MemoryscopeContext = MemoryscopeContext()
+ self._context.memory_scope_uuid = datetime.now().strftime(r"%Y%m%d_%H%M%S")
+ super().__init__(**kwargs)
+ self._init_context_by_config()
+
+ def _init_context_by_config(self):
+ # set global config
+ global_conf = self.config["global"]
+ self._context.language = LanguageEnum(global_conf["language"])
+ self._context.thread_pool = ThreadPoolExecutor(max_workers=global_conf["thread_pool_max_workers"])
+ self._context.meta_data.update({
+ "enable_ranker": global_conf["enable_ranker"],
+ "enable_today_contra_repeat": global_conf["enable_today_contra_repeat"],
+ "enable_long_contra_repeat": global_conf["enable_long_contra_repeat"],
+ "output_memory_max_count": global_conf["output_memory_max_count"],
+ })
+
+ if not global_conf["enable_ranker"]:
+ self.logger.warning("If a semantic ranking model is not available, MemoryScope will use cosine similarity "
+ "scoring as a substitute. However, the ranking effectiveness will be somewhat "
+ "compromised.")
+
+ # init memory_chat
+ memory_chat_conf_dict = self.config["memory_chat"]
+ if memory_chat_conf_dict:
+ for name, conf in memory_chat_conf_dict.items():
+ self._context.memory_chat_dict[name] = init_instance_by_config(conf, name=name, context=self._context)
+
+ # set memory_service
+ memory_service_conf_dict = self.config["memory_service"]
+ assert memory_service_conf_dict
+ for name, conf in memory_service_conf_dict.items():
+ self._context.memory_service_dict[name] = init_instance_by_config(conf, name=name, context=self._context)
+
+ # init model
+ model_conf_dict = self.config["model"]
+ assert model_conf_dict
+ for name, conf in model_conf_dict.items():
+ self._context.model_dict[name] = init_instance_by_config(conf, name=name)
+
+ # init memory_store
+ memory_store_conf = self.config["memory_store"]
+ assert memory_store_conf
+ emb_model_name: str = memory_store_conf[ModelEnum.EMBEDDING_MODEL.value]
+ embedding_model = self._context.model_dict[emb_model_name]
+ self._context.memory_store = init_instance_by_config(memory_store_conf, embedding_model=embedding_model)
+
+ # init monitor
+ monitor_conf = self.config["monitor"]
+ if monitor_conf:
+ self._context.monitor = init_instance_by_config(monitor_conf)
+
+ # set worker config
+ self._context.worker_conf_dict = self.config["worker"]
+
+ def close(self):
+ # wait service to stop
+ for _, service in self._context.memory_service_dict.items():
+ service.stop_backend_service(wait_service=True)
+
+ self._context.thread_pool.shutdown()
+
+ self._context.memory_store.close()
+
+ if self._context.monitor:
+ self._context.monitor.close()
+
+ self.logger.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if exc_type is not None:
+ self.logger.warning(f"An exception occurred: {exc_type.__name__}: {exc_val}\n{exc_tb}")
+ self.close()
+
+ @property
+ def context(self):
+ return self._context
+
+ @property
+ def memory_chat_dict(self):
+ return self._context.memory_chat_dict
+
+ @property
+ def memory_service_dict(self):
+ return self._context.memory_service_dict
+
+ @property
+ def default_memory_chat(self) -> BaseMemoryChat:
+ return list(self.memory_chat_dict.values())[0]
+
+ @property
+ def default_memory_service(self) -> BaseMemoryService:
+ return list(self.memory_service_dict.values())[0]
+
+ @classmethod
+ def cli_memory_chat(cls, **kwargs):
+ with cls(**kwargs) as ms:
+ memory_chat = ms.default_memory_chat
+ memory_chat.run()
diff --git a/memoryscope/memoryscope/core/memoryscope_context.py b/memoryscope/memoryscope/core/memoryscope_context.py
new file mode 100644
index 00000000..6f427f62
--- /dev/null
+++ b/memoryscope/memoryscope/core/memoryscope_context.py
@@ -0,0 +1,53 @@
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass, field
+
+from memoryscope.enumeration.language_enum import LanguageEnum
+from memoryscope.core.utils.singleton import singleton
+
+@singleton
+@dataclass
+class MemoryscopeContext(object):
+ """
+ The context class archives all configs utilized by store, monitor, services and workers.
+ """
+
+ language: LanguageEnum = LanguageEnum.EN
+
+ thread_pool: ThreadPoolExecutor | None = None
+
+ memory_store = None
+
+ monitor = None
+
+ memory_chat_dict: dict = field(default_factory=lambda: {}, metadata={"help": "name -> memory_chat"})
+
+ memory_service_dict: dict = field(default_factory=lambda: {}, metadata={"help": "name -> memory_service"})
+
+ model_dict: dict = field(default_factory=lambda: {}, metadata={"help": "name -> model"})
+
+ worker_conf_dict: dict = field(default_factory=lambda: {}, metadata={"help": "name -> worker_conf"})
+
+ meta_data: dict = field(default_factory=lambda: {})
+
+ memory_scope_uuid: str = ""
+
+ print_workflow_dynamic: bool = False
+
+ log_elasticsearch_dynamic: bool = False
+
+
+def get_memoryscope_uuid():
+ ms_context = MemoryscopeContext()
+ if ms_context.memory_scope_uuid:
+ return ms_context.memory_scope_uuid
+ else:
+ # raise RuntimeError("MemoryscopeContext is not initialized yet. Please initialize it first.")
+ return "memory_scope_uuid_not_registered"
+
+def get_memoryscope_context():
+ ms_context = MemoryscopeContext()
+ if ms_context.memory_scope_uuid:
+ return ms_context
+ else:
+ # raise RuntimeError("MemoryscopeContext is not initialized yet. Please initialize it first.")
+ return "memory_scope_uuid_not_registered"
diff --git a/memoryscope/memoryscope/core/models/__init__.py b/memoryscope/memoryscope/core/models/__init__.py
new file mode 100644
index 00000000..082f5344
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/__init__.py
@@ -0,0 +1,13 @@
+from .base_model import BaseModel
+from .dummy_generation_model import DummyGenerationModel
+from .llama_index_embedding_model import LlamaIndexEmbeddingModel
+from .llama_index_generation_model import LlamaIndexGenerationModel
+from .llama_index_rank_model import LlamaIndexRankModel
+
+__all__ = [
+ "BaseModel",
+ "DummyGenerationModel",
+ "LlamaIndexEmbeddingModel",
+ "LlamaIndexGenerationModel",
+ "LlamaIndexRankModel"
+]
diff --git a/memoryscope/memoryscope/core/models/base_model.py b/memoryscope/memoryscope/core/models/base_model.py
new file mode 100644
index 00000000..05483fee
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/base_model.py
@@ -0,0 +1,121 @@
+import inspect
+import time
+import os
+from abc import abstractmethod, ABCMeta
+from typing import Any
+
+from memoryscope.core.utils.logger import Logger
+from memoryscope.core.utils.registry import Registry
+from memoryscope.core.utils.timer import Timer
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.memoryscope_context import get_memoryscope_uuid
+
+MODEL_REGISTRY = Registry("models")
+
+
+class BaseModel(metaclass=ABCMeta):
+ m_type: ModelEnum | None = None
+
+ def __init__(self,
+ model_name: str,
+ module_name: str,
+ timeout: int = None,
+ max_retries: int = 3,
+ retry_interval: float = 1.0,
+ kwargs_filter: bool = True,
+ raise_exception: bool = True,
+ **kwargs):
+
+ self.model_name: str = model_name
+ self.module_name: str = module_name
+ self.timeout: int = timeout
+ self.max_retries: int = max_retries
+ self.retry_interval: float = retry_interval
+ self.kwargs_filter: bool = kwargs_filter
+ self.raise_exception: bool = raise_exception
+ self.context: MemoryscopeContext = get_memoryscope_uuid()
+ self.kwargs: dict = kwargs
+
+ self._model: Any = None
+ self.logger = Logger.get_logger("base_model")
+
+ @property
+ def model(self):
+ if self._model is None:
+ if self.module_name not in MODEL_REGISTRY.module_dict:
+ raise RuntimeError(f"method_type={self.module_name} is not supported!")
+ obj_cls = MODEL_REGISTRY[self.module_name]
+ if 'openai' in self.module_name:
+ if os.environ.get('OPENAI_API_KEY', None) is None:
+ raise ValueError("Missing openai api key!")
+
+ if self.kwargs_filter:
+ allowed_kwargs = list(inspect.signature(obj_cls.__init__).parameters.keys())
+ kwargs = {key: value for key, value in self.kwargs.items() if key in allowed_kwargs}
+ else:
+ kwargs = self.kwargs
+ self._model = obj_cls(**kwargs)
+
+ return self._model
+
+ @abstractmethod
+ def before_call(self, model_response: ModelResponse, **kwargs):
+ pass
+
+ @abstractmethod
+ def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse | ModelResponseGen:
+ pass
+
+ @abstractmethod
+ def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs):
+ pass
+
+ def call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen:
+ with Timer(self.__class__.__name__, time_log_type="none") as t:
+ model_response = ModelResponse(m_type=self.m_type)
+
+ self.before_call(stream=stream, model_response=model_response, **kwargs)
+ for i in range(self.max_retries):
+ if self.raise_exception:
+ self._call(stream=stream, model_response=model_response, **kwargs)
+ else:
+ try:
+ self._call(stream=stream, model_response=model_response, **kwargs)
+ except Exception as e:
+ model_response.status = False
+ model_response.details = e.args
+
+ if isinstance(model_response, ModelResponse) and not model_response.status:
+ self.logger.warning(f"call model={self.model_name} failed! {t.cost_str} retry_cnt={i} "
+ f"details={model_response.details}", stacklevel=2)
+ time.sleep(i * self.retry_interval)
+ else:
+ return self.after_call(stream=stream, model_response=model_response, **kwargs)
+
+ @abstractmethod
+ async def _async_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
+ pass
+
+ async def async_call(self, **kwargs) -> ModelResponse:
+ with Timer(self.__class__.__name__, time_log_type="none") as t:
+ model_response = ModelResponse(m_type=self.m_type)
+
+ self.before_call(model_response=model_response, **kwargs)
+ for i in range(self.max_retries):
+ if self.raise_exception:
+ await self._async_call(model_response=model_response, **kwargs)
+ else:
+ try:
+ await self._async_call(model_response=model_response, **kwargs)
+ except Exception as e:
+ model_response.status = False
+ model_response.details = e.args
+
+ if not model_response.status:
+ self.logger.warning(f"async_call model={self.model_name} failed! {t.cost_str} retry_cnt={i} "
+ f"details={model_response.details}", stacklevel=2)
+ time.sleep(i * self.retry_interval)
+ else:
+ return self.after_call(model_response=model_response, **kwargs)
diff --git a/memoryscope/memoryscope/core/models/dummy_generation_model.py b/memoryscope/memoryscope/core/models/dummy_generation_model.py
new file mode 100644
index 00000000..0ff3e588
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/dummy_generation_model.py
@@ -0,0 +1,93 @@
+import time
+from typing import List
+
+from llama_index.core.base.llms.types import ChatMessage
+
+from memoryscope.core.models.base_model import BaseModel, MODEL_REGISTRY
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.message import Message
+from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen
+
+
+class DummyGenerationModel(BaseModel):
+ """
+ The `DummyGenerationModel` class serves as a placeholder model for generating responses.
+ It processes input prompts or sequences of messages, adapting them into a structure compatible
+ with chat interfaces. It also facilitates the generation of mock (dummy) responses for testing,
+ supporting both immediate and streamed output.
+ """
+ m_type: ModelEnum = ModelEnum.GENERATION_MODEL
+
+ MODEL_REGISTRY.register("dummy_generation", object)
+
+ def before_call(self, model_response: ModelResponse, **kwargs):
+ """
+ Prepares the input data before making a call to the language model.
+ It accepts either a 'prompt' directly or a list of 'messages'.
+ If 'prompt' is provided, it sets the data accordingly.
+ If 'messages' are provided, it constructs a list of ChatMessage objects from the list.
+ Raises an error if neither 'prompt' nor 'messages' are supplied.
+
+ Args:
+ model_response: model_response
+ **kwargs: Arbitrary keyword arguments including 'prompt' and 'messages'.
+
+ Raises:
+ RuntimeError: When both 'prompt' and 'messages' inputs are not provided.
+ """
+ prompt: str = kwargs.pop("prompt", "")
+ messages: List[Message] | List[dict] = kwargs.pop("messages", [])
+
+ if prompt:
+ data = {"prompt": prompt}
+ elif messages:
+ if isinstance(messages[0], dict):
+ data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]}
+ else:
+ data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]}
+ else:
+ raise RuntimeError("prompt and messages are both empty!")
+ data.update(**kwargs)
+ model_response.meta_data["data"] = data
+
+ def after_call(self,
+ model_response: ModelResponse,
+ stream: bool = False,
+ **kwargs) -> ModelResponse | ModelResponseGen:
+ """
+ Processes the model's response post-call, optionally streaming the output or returning it as a whole.
+
+ This method modifies the input `model_response` by resetting its message content and, based on the `stream`
+ parameter, either yields the response in a generated stream or returns the complete response directly.
+
+ Args:
+ model_response (ModelResponse): The initial response object to be processed.
+ stream (bool, optional): Flag indicating whether to stream the response. Defaults to False.
+ **kwargs: Additional keyword arguments (not used in this implementation).
+
+ Returns:
+ ModelResponse | ModelResponseGen: If `stream` is True, a generator yielding updated `ModelResponse` objects;
+ otherwise, a modified `ModelResponse` object with the complete content.
+ """
+ model_response.message = Message(role=MessageRoleEnum.ASSISTANT, content="")
+
+ call_result = ["-" for _ in range(10)]
+ if stream:
+ def gen() -> ModelResponseGen:
+ for delta in call_result:
+ model_response.message.content += delta
+ model_response.delta = delta
+ time.sleep(0.1)
+ yield model_response
+
+ return gen()
+ else:
+ model_response.message.content = "".join(call_result)
+ return model_response
+
+ def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs):
+ return model_response
+
+ async def _async_call(self, model_response: ModelResponse, **kwargs):
+ return model_response
diff --git a/memoryscope/memoryscope/core/models/llama_index_embedding_model.py b/memoryscope/memoryscope/core/models/llama_index_embedding_model.py
new file mode 100644
index 00000000..88a733b5
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/llama_index_embedding_model.py
@@ -0,0 +1,87 @@
+from typing import List
+
+from llama_index.embeddings.dashscope import DashScopeEmbedding
+from llama_index.embeddings.openai import OpenAIEmbedding
+
+from memoryscope.core.models.base_model import BaseModel, MODEL_REGISTRY
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.model_response import ModelResponse
+from memoryscope.core.utils.logger import Logger
+
+
+class LlamaIndexEmbeddingModel(BaseModel):
+ """
+ Manages text embeddings utilizing the DashScopeEmbedding within the LlamaIndex framework,
+ facilitating embedding operations for both sync and async modes, inheriting from BaseModel.
+ """
+ m_type: ModelEnum = ModelEnum.EMBEDDING_MODEL
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.logger = Logger.get_logger("llama_index_embedding_model")
+
+ @classmethod
+ def register_model(cls, model_name: str, model_class: type):
+ """
+ Registers a new embedding model class with the model registry.
+
+ Args:
+ model_name (str): The name to register the model under.
+ model_class (type): The class of the model to register.
+ """
+ MODEL_REGISTRY.register(model_name, model_class)
+
+ MODEL_REGISTRY.register("dashscope_embedding", DashScopeEmbedding)
+ MODEL_REGISTRY.register("openai_embedding", OpenAIEmbedding)
+
+ def before_call(self, model_response: ModelResponse, **kwargs):
+ text: str | List[str] = kwargs.pop("text", "")
+ if isinstance(text, str):
+ text = [text]
+ model_response.meta_data["data"] = dict(texts=text)
+ self.logger.info("Embedding Model:\n" + text[0])
+
+ def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
+ embeddings = model_response.raw
+ if not embeddings:
+ model_response.details = "empty embeddings"
+ model_response.status = False
+ return model_response
+ if len(embeddings) == 1:
+ # return list[float]
+ embeddings = embeddings[0]
+
+ model_response.embedding_results = embeddings
+ return model_response
+
+ def _call(self, model_response: ModelResponse, **kwargs):
+ """
+ Executes a synchronous call to generate embeddings for the input data.
+
+ This method utilizes the `get_text_embedding_batch` method of the encapsulated model,
+ passing the processed data from `self.data`. The result is then packaged into a
+ `ModelResponse` object with the model type specified by `self.m_type`.
+
+ Args:
+ **kwargs: Additional keyword arguments that might be used in the embedding process.
+
+ Returns:
+ ModelResponse: An object containing the embedding results and the model type.
+ """
+ model_response.raw = self.model.get_text_embedding_batch(**model_response.meta_data["data"])
+
+ async def _async_call(self, model_response: ModelResponse, **kwargs):
+ """
+ Executes an asynchronous call to generate embeddings for the input data.
+
+ Similar to `_call`, but uses the asynchronous `aget_text_embedding_batch` method
+ of the model. It handles the input data asynchronously and packages the result
+ within a `ModelResponse` instance.
+
+ Args:
+ **kwargs: Additional keyword arguments for the embedding process, if any.
+
+ Returns:
+ ModelResponse: An object encapsulating the embedding output and the model's type.
+ """
+ model_response.raw = await self.model.aget_text_embedding_batch(**model_response.meta_data["data"])
diff --git a/memoryscope/memoryscope/core/models/llama_index_generation_model.py b/memoryscope/memoryscope/core/models/llama_index_generation_model.py
new file mode 100644
index 00000000..05941e13
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/llama_index_generation_model.py
@@ -0,0 +1,128 @@
+from typing import List
+
+from llama_index.core.base.llms.types import ChatMessage, ChatResponse, CompletionResponse
+from llama_index.llms.dashscope import DashScope
+from llama_index.llms.openai import OpenAI
+
+from memoryscope.core.models.base_model import BaseModel, MODEL_REGISTRY
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.message import Message
+from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen
+from memoryscope.core.utils.logger import Logger
+
+
+class LlamaIndexGenerationModel(BaseModel):
+ """
+ This class represents a generation model within the LlamaIndex framework,
+ capable of processing input prompts or message histories, selecting an appropriate
+ language model service from a registry, and generating text responses, with support
+ for both streaming and non-streaming modes. It encapsulates logic for formatting
+ these interactions within the context of a memory scope management system.
+ """
+
+ m_type: ModelEnum = ModelEnum.GENERATION_MODEL
+
+ MODEL_REGISTRY.register("dashscope_generation", DashScope)
+ MODEL_REGISTRY.register("openai_generation", OpenAI)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.logger = Logger.get_logger("llama_index_generation_model")
+
+ def before_call(self, model_response: ModelResponse, **kwargs):
+ """
+ Prepares the input data before making a call to the language model.
+ It accepts either a 'prompt' directly or a list of 'messages'.
+ If 'prompt' is provided, it sets the data accordingly.
+ If 'messages' are provided, it constructs a list of ChatMessage objects from the list.
+ Raises an error if neither 'prompt' nor 'messages' are supplied.
+
+ Args:
+ model_response: model_response
+ **kwargs: Arbitrary keyword arguments including 'prompt' and 'messages'.
+
+ Raises:
+ RuntimeError: When both 'prompt' and 'messages' inputs are not provided.
+ """
+ prompt: str = kwargs.pop("prompt", "")
+ messages: List[Message] | List[dict] = kwargs.pop("messages", [])
+
+ if prompt:
+ data = {"prompt": prompt}
+ elif messages:
+ if isinstance(messages[0], dict):
+ data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]}
+ else:
+ data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]}
+ else:
+ raise RuntimeError("prompt and messages are both empty!")
+ data.update(**kwargs)
+ model_response.meta_data["data"] = data
+
+ def after_call(self,
+ model_response: ModelResponse,
+ stream: bool = False,
+ **kwargs) -> ModelResponse | ModelResponseGen:
+ model_response.message = Message(role=MessageRoleEnum.ASSISTANT, content="")
+
+ call_result = model_response.raw
+ if stream:
+ def gen() -> ModelResponseGen:
+ for response in call_result:
+ delta = response.delta if response.delta else ""
+ model_response.message.content += delta
+ model_response.delta = response.delta
+ yield model_response
+ self.logger.info(self.logger.format_chat_message(model_response))
+ return gen()
+ else:
+ if isinstance(call_result, CompletionResponse):
+ model_response.message.content = call_result.text
+ elif isinstance(call_result, ChatResponse):
+ model_response.message.content = call_result.message.content
+ else:
+ raise NotImplementedError
+ self.logger.info(self.logger.format_chat_message(model_response))
+ return model_response
+
+ def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs):
+ data = model_response.meta_data["data"]
+ # FIXME: special case for OpenAI model, is this necessary?
+ data.pop("stream")
+ if "prompt" in data:
+ if stream:
+ model_response.raw = self.model.stream_complete(**data)
+ else:
+ model_response.raw = self.model.complete(**data)
+ elif "messages" in data:
+ if stream:
+ model_response.raw = self.model.stream_chat(**data)
+ else:
+ model_response.raw = self.model.chat(**data)
+ else:
+ raise RuntimeError("prompt or messages is missing!")
+
+ async def _async_call(self, model_response: ModelResponse, **kwargs):
+ """
+ Asynchronously calls the language model with the provided prompt or message history,
+ and packages the raw response into a ModelResponse object.
+
+ This method checks if the input data contains a 'prompt' or 'messages' key to decide
+ which method to call on the model instance. It uses 'acomplete' for simple prompts and
+ 'achat' for chat-based message histories.
+
+ Args:
+ **kwargs: Additional keyword arguments that might be used in the model call.
+
+ Returns:
+ ModelResponse: An object containing the raw response from the language model.
+ """
+ data = model_response.meta_data["data"]
+
+ if "prompt" in data:
+ model_response.raw = await self.model.acomplete(**data)
+ elif "messages" in data:
+ model_response.raw = await self.model.achat(**data)
+ else:
+ raise RuntimeError("prompt or messages is missing!")
diff --git a/memoryscope/memoryscope/core/models/llama_index_rank_model.py b/memoryscope/memoryscope/core/models/llama_index_rank_model.py
new file mode 100644
index 00000000..e290e0cc
--- /dev/null
+++ b/memoryscope/memoryscope/core/models/llama_index_rank_model.py
@@ -0,0 +1,101 @@
+from typing import List
+
+from llama_index.core.data_structs import Node
+from llama_index.core.schema import NodeWithScore
+from llama_index.postprocessor.dashscope_rerank import DashScopeRerank
+
+from memoryscope.core.models.base_model import BaseModel, MODEL_REGISTRY
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.model_response import ModelResponse
+from memoryscope.core.utils.logger import Logger
+
+
+class LlamaIndexRankModel(BaseModel):
+ """
+ The LlamaIndexRankModel class is designed to rerank documents according to their relevance
+ to a provided query, utilizing the DashScope Rerank model. It transforms document lists
+ and queries into a compatible format for ranking, manages the ranking process, and allocates
+ rank scores to individual documents.
+ """
+ m_type: ModelEnum = ModelEnum.RANK_MODEL
+
+ MODEL_REGISTRY.register("dashscope_rank", DashScopeRerank)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.logger = Logger.get_logger("llama_index_rank_model")
+
+ def before_call(self, model_response: ModelResponse, **kwargs):
+ """
+ Prepares necessary data before the ranking call by extracting the query and documents,
+ ensuring they are valid, and initializing nodes with dummy scores.
+
+ Args:
+ model_response: model response
+ **kwargs: Keyword arguments containing 'query' and 'documents'.
+ """
+ query: str = kwargs.pop("query", "")
+ documents: List[str] = kwargs.pop("documents", [])
+ if isinstance(documents, str):
+ documents = [documents]
+ assert query and documents and all(documents), \
+ f"query or documents is empty! query={query}, documents={len(documents)}"
+ assert len(documents) < 500, \
+ "The input documents of Dashscope rerank model should not larger than 500!"
+ # Using -1.0 as dummy scores
+ nodes = [NodeWithScore(node=Node(text=doc), score=-1.0) for doc in documents]
+
+ model_response.meta_data.update({
+ "data": {"nodes": nodes, "query_str": query, "top_n": len(documents)},
+ "documents_map": {doc: idx for idx, doc in enumerate(documents)},
+ })
+
+ def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
+ """
+ Processes the model response post-ranking, assigning calculated rank scores to each document
+ based on their index in the original document list.
+
+ Args:
+ model_response (ModelResponse): The initial response from the ranking model.
+ **kwargs: Additional keyword arguments (unused).
+
+ Returns:
+ ModelResponse: Updated response with rank scores assigned to documents.
+ """
+ if not model_response.rank_scores:
+ model_response.rank_scores = {}
+
+ documents_map = model_response.meta_data["documents_map"]
+ for node in model_response.raw:
+ text = node.node.text
+ idx = documents_map[text]
+ model_response.rank_scores[idx] = node.score
+
+ self.logger.info(self.logger.format_rank_message(model_response))
+ return model_response
+
+ def _call(self, model_response: ModelResponse, **kwargs):
+ """
+ Executes the ranking process by passing prepared data to the model's postprocessing method.
+
+ Args:
+ **kwargs: Keyword arguments (unused).
+
+ Returns:
+ ModelResponse: A response object encapsulating the ranked nodes.
+ """
+ self.model.top_n = model_response.meta_data["data"]["top_n"]
+ model_response.meta_data["data"].pop("top_n")
+ model_response.raw = self.model.postprocess_nodes(**model_response.meta_data["data"])
+
+ async def _async_call(self, **kwargs) -> ModelResponse:
+ """
+ Asynchronous wrapper for the `_call` method, maintaining the same functionality.
+
+ Args:
+ **kwargs: Keyword arguments (unused).
+
+ Returns:
+ ModelResponse: A response object encapsulating the ranked nodes.
+ """
+ raise NotImplementedError
diff --git a/memoryscope/memoryscope/core/operation/__init__.py b/memoryscope/memoryscope/core/operation/__init__.py
new file mode 100644
index 00000000..96167026
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/__init__.py
@@ -0,0 +1,13 @@
+from .backend_operation import BackendOperation
+from .base_operation import BaseOperation
+from .base_workflow import BaseWorkflow
+from .consolidate_memory_op import ConsolidateMemoryOp
+from .frontend_operation import FrontendOperation
+
+__all__ = [
+ "BackendOperation",
+ "BaseOperation",
+ "BaseWorkflow",
+ "ConsolidateMemoryOp",
+ "FrontendOperation"
+]
diff --git a/memoryscope/memoryscope/core/operation/backend_operation.py b/memoryscope/memoryscope/core/operation/backend_operation.py
new file mode 100644
index 00000000..27b0d5e5
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/backend_operation.py
@@ -0,0 +1,83 @@
+import time
+import threading
+
+from memoryscope.core.operation.base_operation import OPERATION_TYPE
+from memoryscope.core.operation.frontend_operation import FrontendOperation
+
+
+class BackendOperation(FrontendOperation):
+ """
+ BaseBackendOperation serves as an abstract base class for defining backend operations.
+ It manages operation status, loop control, and integrates with a global context for thread management.
+ """
+ operation_type: OPERATION_TYPE = "backend"
+
+ def __init__(self, interval_time: int, **kwargs):
+ super().__init__(**kwargs)
+
+ self._interval_time: int = interval_time
+
+ self._operation_status_run: bool = False
+ self._loop_switch: bool = False
+ self._backend_task = None
+
+ def init_workflow(self, **kwargs):
+ """
+ Initializes the workflow by setting up workers with provided keyword arguments.
+
+ Args:
+ **kwargs: Arbitrary keyword arguments to be passed during worker initialization.
+ """
+ self.init_workers(is_backend=True, **kwargs)
+
+ def _loop_operation(self, **kwargs):
+ """
+ Loops until _loop_switch is False, sleeping for 1 second in each interval.
+ At each interval, it checks if _loop_switch is still True, and if so, executes the operation.
+ """
+ while self._loop_switch:
+ for _ in range(self._interval_time):
+ if self._loop_switch:
+ time.sleep(1)
+ else:
+ break
+
+ if self._loop_switch:
+ if self._operation_status_run:
+ continue
+
+ self._operation_status_run = True
+
+ if len(self.target_names) > 1:
+ self.logger.warning("current version is not stable under target_names.size > 1!")
+
+ for target_name in self.target_names:
+ try:
+ self.run_operation(target_name=target_name, **kwargs)
+ except Exception as e:
+ self.logger.exception(f"op_name={self.name} target_name={target_name} encounter exception. "
+ f"args={e.args}")
+
+ self._operation_status_run = False
+
+ def start_operation_backend(self, **kwargs):
+ """
+ Initiates the background operation loop if it's not already running.
+ Sets the _loop_switch to True and submits the _loop_operation to a thread from the global thread pool.
+ """
+ if not self._loop_switch:
+ self._loop_switch = True
+ self._backend_task = self.thread_pool.submit(self._loop_operation, **kwargs)
+ self.logger.info(f"start operation={self.name}...")
+
+ def stop_operation_backend(self, wait_operation: bool = False):
+ """
+ Stops the background operation loop by setting the _loop_switch to False.
+ """
+ self._loop_switch = False
+ if self._backend_task:
+ if wait_operation:
+ self._backend_task.result()
+ self.logger.info(f"stop operation={self.name}...")
+ else:
+ self.logger.info(f"send stop signal to operation={self.name}...")
diff --git a/memoryscope/memoryscope/core/operation/base_operation.py b/memoryscope/memoryscope/core/operation/base_operation.py
new file mode 100644
index 00000000..2661c5f2
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/base_operation.py
@@ -0,0 +1,72 @@
+from abc import ABCMeta, abstractmethod
+from typing import Literal, List
+
+from memoryscope.scheme.message import Message
+
+OPERATION_TYPE = Literal["frontend", "backend"]
+
+
+class BaseOperation(metaclass=ABCMeta):
+ """
+ An abstract base class representing an operation that can be categorized as either frontend or backend.
+
+ Attributes:
+ operation_type (OPERATION_TYPE): Specifies the type of operation, defaulting to "frontend".
+ name (str): The name of the operation.
+ description (str): A description of the operation.
+ """
+
+ operation_type: OPERATION_TYPE = "frontend"
+
+ def __init__(self,
+ name: str,
+ user_name: str,
+ target_names: List[str],
+ chat_messages: List[List[Message]],
+ description: str):
+ """
+ Initializes a new instance of the BaseOperation.
+ """
+ self.name: str = name
+ self.user_name: str = user_name
+ self.target_names: List[str] = target_names
+ self.chat_messages: List[List[Message]] = chat_messages
+ self.description: str = description
+
+ def init_workflow(self, **kwargs):
+ """
+ Initialize the workflow with additional keyword arguments if needed.
+
+ Args:
+ **kwargs: Additional parameters for initializing the workflow.
+ """
+ pass
+
+ @abstractmethod
+ def run_operation(self, target_name: str, **kwargs):
+ """
+ Abstract method to define the operation to be run.
+ Subclasses must implement this method.
+
+ Args:
+ target_name (str): target_name(human name).
+ **kwargs: Keyword arguments for running the operation.
+
+ Raises:
+ NotImplementedError: If the subclass does not implement this method.
+ """
+ raise NotImplementedError
+
+ def start_operation_backend(self, **kwargs):
+ """
+ Placeholder method for running an operation specific to the backend.
+ Intended to be overridden by subclasses if backend operations are required.
+ """
+ pass
+
+ def stop_operation_backend(self, wait_operation: bool = False):
+ """
+ Placeholder method to stop any ongoing backend operations.
+ Should be implemented in subclasses where backend operations are managed.
+ """
+ pass
diff --git a/memoryscope/memoryscope/core/operation/base_workflow.py b/memoryscope/memoryscope/core/operation/base_workflow.py
new file mode 100644
index 00000000..4f8b6156
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/base_workflow.py
@@ -0,0 +1,207 @@
+import re
+import threading
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from itertools import zip_longest
+from typing import Dict, Any, List
+from rich.console import Console
+
+from memoryscope.constants.common_constants import WORKFLOW_NAME
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.utils.logger import Logger
+from memoryscope.core.utils.timer import Timer
+from memoryscope.core.utils.tool_functions import init_instance_by_config
+from memoryscope.core.worker.base_worker import BaseWorker
+
+class BaseWorkflow(object):
+
+ def __init__(self,
+ name: str,
+ memoryscope_context: MemoryscopeContext,
+ workflow: str = "",
+ **kwargs):
+
+ self.name: str = name
+ self.memoryscope_context: MemoryscopeContext = memoryscope_context
+ self.thread_pool: ThreadPoolExecutor = self.memoryscope_context.thread_pool
+ self.workflow: str = workflow
+ self.kwargs = kwargs
+
+ self.workflow_worker_list: List[List[List[str]]] = []
+ self.worker_dict: Dict[str, BaseWorker | bool] = {}
+ self.workflow_context: Dict[str, Any] = {}
+ self.context_lock = threading.Lock()
+
+ self.logger: Logger = Logger.get_logger("workflow")
+
+ if self.workflow:
+ self.workflow_worker_list = self._parse_workflow()
+ self._print_workflow()
+
+ def workflow_print_console(self, *args, **kwargs):
+ if self.memoryscope_context.print_workflow_dynamic:
+ Console().print(*args, **kwargs)
+ return
+
+ def _parse_workflow(self):
+ """
+ Parses the workflow string to configure worker threads and organizes them into execution order.
+
+ The workflow string format supports complex configurations with optional multi-threading indications.
+ E.g., `[task1,task2|task3],task4` denotes task1 and task2 can run in parallel to task3, followed by task4.
+
+ Returns:
+ List[List[List[str]]]: A nested list representing the execution plan, including parallel groups and tasks.
+ """
+ # Regular expression to match components of the workflow, handling both plain items and grouped items.
+ pattern = r"(\[[^\]]*\]|[^,]+)"
+ # Find all matches in the workflow string based on the pattern.
+ workflow_split = re.findall(pattern, self.workflow)
+
+ for workflow_part in workflow_split:
+ # e.g., [d,e,f|g,h]
+ workflow_part = workflow_part.strip()
+ if '[' in workflow_part or ']' in workflow_part:
+ workflow_part = workflow_part.replace('[', '').replace(']', '')
+
+ # Split the part by '|' to identify potential parallel task groups.
+ line_split = [x.strip() for x in workflow_part.split("|") if x]
+
+ # Skip if no valid tasks are identified after splitting.
+ if len(line_split) <= 0:
+ continue
+
+ # Determine if the current part involves multi-threading based on the number of groups.
+ is_multi_thread: bool = len(line_split) > 1
+
+ # e.g., ["d","e","f"]
+ line_split_split: List[List[str]] = []
+ for sub_line_split in line_split:
+ sub_split = [x.strip() for x in sub_line_split.split(",")]
+ line_split_split.append(sub_split)
+ # add workers
+ for sub_item in sub_split:
+ self.worker_dict[sub_item] = is_multi_thread
+
+ # Append the parsed and structured tasks to the workflow execution plan.
+ self.workflow_worker_list.append(line_split_split)
+
+ # Return the fully constructed workflow execution plan.
+ return self.workflow_worker_list
+
+ def _print_workflow(self):
+ """
+ Prints the workflow stages in a structured format. Each stage of the workflow
+ is detailed with its constituent parts, either single elements or grouped
+ elements separated by ' | '.
+
+ The method iterates over the workflow parts, handling both singular steps
+ and parallel steps (where elements are zipped together).
+ """
+ self.logger.info(f"----- workflow.{self.name}.print.begin -----")
+ i: int = 0
+ for workflow_part in self.workflow_worker_list:
+ if len(workflow_part) == 1:
+ # Handles workflow parts with single elements
+ for w in workflow_part[0]:
+ self.logger.info(f"stage{i}: {w}")
+ i += 1
+ else:
+ # Handles workflow parts with multiple parallel elements (zipped)
+ for w_zip in zip_longest(*workflow_part, fillvalue="-"):
+ self.logger.info(f"stage{i}: {' | '.join(w_zip)}")
+ i += 1
+ # Skips placeholder '-' used for uneven lists in zip_longest
+ for w in w_zip:
+ if w == "-":
+ continue
+ self.logger.info(f"----- workflow.{self.name}.print.end -----")
+
+ def init_workers(self, is_backend: bool = False, **kwargs):
+ """
+ Initializes worker instances based on the configuration for each worker defined in `G_CONTEXT.worker_config`.
+ Each worker can be set to run in a multithreaded mode depending on the `is_backend` flag or the worker's
+ individual configuration.
+
+ Args:
+ is_backend (bool, optional): A flag indicating whether the workers should be initialized in a
+ backend context. Defaults to False.
+ **kwargs: Additional keyword arguments to be passed during worker initialization.
+
+ Raises:
+ RuntimeError: If a worker mentioned in `self.worker_dict` does not exist in `G_CONTEXT.worker_config`.
+
+ Note:
+ This method modifies `self.worker_dict` in-place, replacing the keys with actual worker instances.
+ """
+ for name in list(self.worker_dict.keys()):
+ if name not in self.memoryscope_context.worker_conf_dict:
+ raise RuntimeError(f"worker={name} is not exists in worker config!")
+
+ # note: shared context object in all workers
+ self.worker_dict[name] = init_instance_by_config(
+ config=self.memoryscope_context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=is_backend or self.worker_dict[name],
+ context=self.workflow_context,
+ memoryscope_context=self.memoryscope_context,
+ context_lock=self.context_lock,
+ thread_pool=self.thread_pool,
+ **kwargs)
+
+ def _run_sub_workflow(self, worker_list: List[str]) -> bool:
+ for name in worker_list:
+ worker = self.worker_dict[name]
+ worker.run()
+ if not worker.continue_run:
+ self.logger.warning(f"worker={worker.name} stop workflow!")
+ return False
+ return True
+
+ def run_workflow(self, **kwargs):
+ """
+ Executes the workflow by orchestrating the steps defined in `self.workflow_worker_list`.
+ This method supports both sequential and parallel execution of sub-workflows based on the structure
+ of `self.workflow_worker_list`.
+
+ If a workflow part consists of a single item, it is executed sequentially. For parts with multiple items,
+ they are submitted for parallel execution using a thread pool. The workflow will stop if any sub-workflow
+ returns False.
+
+ Args:
+ **kwargs: Additional keyword arguments to be passed to context.
+ """
+ with Timer(f"workflow.{self.name}", time_log_type="wrap"):
+ log_buf = f"Operation: {self.name}"
+ self.logger.info(log_buf)
+ self.workflow_print_console(log_buf, style="bold red")
+ self.workflow_context.clear()
+ self.workflow_context.update({WORKFLOW_NAME: self.name, **kwargs})
+ n_stage = len(self.workflow_worker_list)
+ # Iterate over each part of the workflow
+ for index, workflow_part in enumerate(self.workflow_worker_list):
+ # self.logger.info(self.logger.format_current_context(self.workflow_context))
+ # Sequential execution for single-item parts
+ if len(workflow_part) == 1:
+ log_buf = f"\t- Operation: {self.name} | {index+1}/{n_stage}: {workflow_part[0]}"
+ self.logger.info(log_buf)
+ self.workflow_print_console(log_buf, style="bold red")
+ if not self._run_sub_workflow(workflow_part[0]):
+ break
+ # Parallel execution for multi-item parts
+ else:
+ t_list = []
+ # Submit tasks to the thread pool
+ n_sub_stage = len(workflow_part)
+ for sub_index, sub_workflow in enumerate(workflow_part):
+ log_buf = f"\t- Operation: {self.name} | {index+1}/{n_stage} | sub workflow {sub_index+1}/{n_sub_stage}: {str(sub_workflow)}"
+ self.logger.info(log_buf)
+ self.workflow_print_console(log_buf, style="red")
+ t_list.append(self.thread_pool.submit(self._run_sub_workflow, sub_workflow))
+
+ # Check results; if any task returns False, stop the workflow
+ flag = True
+ for future in as_completed(t_list):
+ if not future.result():
+ flag = False
+ if not flag:
+ break
diff --git a/memoryscope/memoryscope/core/operation/consolidate_memory_op.py b/memoryscope/memoryscope/core/operation/consolidate_memory_op.py
new file mode 100644
index 00000000..d10bb7d3
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/consolidate_memory_op.py
@@ -0,0 +1,81 @@
+from typing import List
+
+from memoryscope.constants.common_constants import CHAT_KWARGS, CHAT_MESSAGES, RESULT, TARGET_NAME, USER_NAME
+from memoryscope.core.operation.backend_operation import BackendOperation
+from memoryscope.scheme.message import Message
+
+
+class ConsolidateMemoryOp(BackendOperation):
+
+ def __init__(self,
+ message_lock,
+ contextual_msg_min_count: int = 0,
+ **kwargs):
+ super().__init__(**kwargs)
+ self.message_lock = message_lock
+ self.contextual_msg_min_count: int = contextual_msg_min_count
+
+ def run_operation(self, target_name: str, **kwargs):
+ """
+ Executes an operation after preparing the chat context, checking message memory status,
+ and updating workflow status accordingly.
+
+ If the number of not-memorized messages is less than the contextual message count,
+ the operation is skipped. Otherwise, it sets up the chat context, runs the workflow,
+ captures the result, and updates the memory status.
+
+ Args:
+ target_name (str): target_name(human name).
+ **kwargs: Keyword arguments for chat operation configuration.
+
+ Returns:
+ Any: The result obtained from running the workflow.
+ """
+
+ chat_messages: List[List[Message]] = []
+ for messages in self.chat_messages:
+ if not messages:
+ continue
+
+ if messages[0].memorized:
+ continue
+
+ contain_flag = False
+
+ for msg in messages:
+ if msg.role_name == target_name:
+ contain_flag = True
+ break
+
+ if contain_flag:
+ chat_messages.append(messages)
+
+ if not chat_messages:
+ self.logger.info(f"empty not_memorized chat_messages for target_name={target_name}.")
+ return
+
+ if len(chat_messages) < self.contextual_msg_min_count:
+ self.logger.info(f"not_memorized_size={len(chat_messages)} < {self.contextual_msg_min_count}, skip.")
+ return
+
+ # prepare kwargs
+ workflow_kwargs = {
+ CHAT_MESSAGES: chat_messages,
+ CHAT_KWARGS: {**kwargs, **self.kwargs},
+ TARGET_NAME: target_name,
+ USER_NAME: self.user_name,
+ }
+
+ # Execute the workflow with the prepared context
+ self.run_workflow(**workflow_kwargs)
+
+ # Retrieve the result from the context after workflow execution
+ result = self.workflow_context.get(RESULT)
+
+ # set message memorized
+ with self.message_lock:
+ for messages in chat_messages:
+ for msg in messages:
+ msg.memorized = True
+
+ return result
diff --git a/memoryscope/memoryscope/core/operation/frontend_operation.py b/memoryscope/memoryscope/core/operation/frontend_operation.py
new file mode 100644
index 00000000..ea9b4b9f
--- /dev/null
+++ b/memoryscope/memoryscope/core/operation/frontend_operation.py
@@ -0,0 +1,61 @@
+from typing import List
+
+from memoryscope.constants.common_constants import RESULT, CHAT_MESSAGES, CHAT_KWARGS, TARGET_NAME, USER_NAME
+from memoryscope.core.operation.base_operation import BaseOperation, OPERATION_TYPE
+from memoryscope.core.operation.base_workflow import BaseWorkflow
+from memoryscope.scheme.message import Message
+
+
+class FrontendOperation(BaseWorkflow, BaseOperation):
+ operation_type: OPERATION_TYPE = "frontend"
+
+ def __init__(self,
+ name: str,
+ user_name: str,
+ target_names: List[str],
+ chat_messages: List[List[Message]],
+ description: str,
+ **kwargs):
+ super().__init__(name=name, **kwargs)
+ BaseOperation.__init__(self,
+ name=name,
+ user_name=user_name,
+ target_names=target_names,
+ chat_messages=chat_messages,
+ description=description)
+
+ def init_workflow(self, **kwargs):
+ """
+ Initializes the workflow by setting up workers with provided keyword arguments.
+
+ Args:
+ **kwargs: Arbitrary keyword arguments to be passed during worker initialization.
+ """
+ self.init_workers(**kwargs)
+
+ def run_operation(self, target_name: str, **kwargs):
+ """
+ Executes the main operation of reading recent chat messages, initializing workflow,
+ and returning the result of the workflow execution.
+
+ Args:
+ target_name (str): target_name(human name).
+ **kwargs: Additional keyword arguments used in the operation context.
+
+ Returns:
+ Any: The result obtained from executing the workflow.
+ """
+
+ # prepare kwargs
+ workflow_kwargs = {
+ CHAT_MESSAGES: self.chat_messages,
+ CHAT_KWARGS: {**kwargs, **self.kwargs},
+ TARGET_NAME: target_name,
+ USER_NAME: self.user_name,
+ }
+
+ # Execute the workflow with the prepared context
+ self.run_workflow(**workflow_kwargs)
+
+ # Retrieve the result from the context after workflow execution
+ return self.workflow_context.get(RESULT)
diff --git a/memoryscope/memoryscope/core/service/__init__.py b/memoryscope/memoryscope/core/service/__init__.py
new file mode 100644
index 00000000..6799a9c9
--- /dev/null
+++ b/memoryscope/memoryscope/core/service/__init__.py
@@ -0,0 +1,7 @@
+from .base_memory_service import BaseMemoryService
+from .memory_scope_service import MemoryScopeService
+
+__all__ = [
+ "BaseMemoryService",
+ "MemoryScopeService"
+]
diff --git a/memoryscope/memoryscope/core/service/base_memory_service.py b/memoryscope/memoryscope/core/service/base_memory_service.py
new file mode 100644
index 00000000..72dbfb1d
--- /dev/null
+++ b/memoryscope/memoryscope/core/service/base_memory_service.py
@@ -0,0 +1,99 @@
+from abc import ABCMeta, abstractmethod
+from typing import List, Dict
+
+from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.operation.base_operation import BaseOperation
+from memoryscope.core.utils.logger import Logger
+from memoryscope.scheme.message import Message
+
+
+class BaseMemoryService(metaclass=ABCMeta):
+ """
+ An abstract base class for managing memory operations within a multithreaded context.
+ It sets up the infrastructure for operation handling, message storage, and synchronization,
+ along with logging capabilities and customizable configurations.
+ """
+
+ def __init__(self,
+ memory_operations: Dict[str, dict],
+ context: MemoryscopeContext,
+ assistant_name: str = None,
+ human_name: str = None,
+ **kwargs):
+ """
+ Initializes the BaseMemoryService with operation definitions, keys for memory access,
+ and additional keyword arguments for flexibility.
+
+ Args:
+ memory_operations (Dict[str, dict]): A dictionary defining available memory operations.
+ context (MemoryscopeContext): runtime context.
+ human_name (str): human name.
+ assistant_name (str): assistant name.
+ **kwargs: Additional parameters to customize service behavior.
+ """
+ self._operations_conf: Dict[str, dict] = memory_operations
+ self._context: MemoryscopeContext = context
+ self._human_name: str = human_name
+ self._assistant_name: str = assistant_name
+ self._kwargs = kwargs
+
+ if not self._human_name:
+ self._human_name = DEFAULT_HUMAN_NAME[self._context.language]
+ if not self._assistant_name:
+ self._assistant_name = "AI"
+
+ self._operation_dict: Dict[str, BaseOperation] = {}
+ self._chat_messages: List[List[Message]] = []
+ self._role_names: List[str] = []
+
+ self.logger = Logger.get_logger()
+
+ @property
+ def human_name(self) -> str:
+ return self._human_name
+
+ @property
+ def assistant_name(self) -> str:
+ return self._assistant_name
+
+ def get_chat_messages_scatter(self, recent_n_pair: int) -> List[Message]:
+ chat_messages_scatter: List[Message] = []
+ for messages in self._chat_messages[-recent_n_pair:]:
+ chat_messages_scatter.extend(messages)
+ return chat_messages_scatter
+
+ @property
+ def op_description_dict(self) -> Dict[str, str]:
+ """
+ Property to retrieve a dictionary mapping operation keys to their descriptions.
+ Returns:
+ Dict[str, str]: A dictionary where keys are operation identifiers and values are their descriptions.
+ """
+ return {k: v.description for k, v in self._operation_dict.items()}
+
+ @abstractmethod
+ def add_messages_pair(self, messages: List[Message]):
+ raise NotImplementedError
+
+ @abstractmethod
+ def register_operation(self, name: str, operation_config: dict, **kwargs):
+ raise NotImplementedError
+
+ @abstractmethod
+ def init_service(self, **kwargs):
+ raise NotImplementedError
+
+ def start_backend_service(self, name: str = None, **kwargs):
+ pass
+
+ def stop_backend_service(self, wait_service: bool = False):
+ pass
+
+ @abstractmethod
+ def run_operation(self, name: str, role_name: str = "", **kwargs):
+ raise NotImplementedError
+
+ def __getattr__(self, name: str):
+ assert name in self._operation_dict, f"operation={name} is not registered!"
+ return lambda **kwargs: self.run_operation(name=name, **kwargs)
diff --git a/memoryscope/memoryscope/core/service/memory_scope_service.py b/memoryscope/memoryscope/core/service/memory_scope_service.py
new file mode 100644
index 00000000..ec6c4a79
--- /dev/null
+++ b/memoryscope/memoryscope/core/service/memory_scope_service.py
@@ -0,0 +1,134 @@
+import threading
+from typing import List
+
+from memoryscope.core.operation.base_operation import BaseOperation
+from memoryscope.core.service.base_memory_service import BaseMemoryService
+from memoryscope.core.utils.tool_functions import init_instance_by_config
+from memoryscope.scheme.message import Message
+
+
+class MemoryScopeService(BaseMemoryService):
+ def __init__(self,
+ history_msg_count: int = 100,
+ contextual_msg_max_count: int = 10,
+ contextual_msg_min_count: int = 0,
+ **kwargs):
+ """
+ init function.
+ Args:
+ history_msg_count (int): The conversation history in memory, control the quantity, and reduce memory usage.
+ contextual_msg_max_count (int): The maximum context length in a conversation. If it exceeds this length,
+ it will not be included in the context to prevent token overflow.
+ contextual_msg_min_count (int): The minimum context length in a conversation. If it is shorter than this
+ length, no conversation summary will be made and no long-term memory will be generated.
+ kwargs (dict): Additional parameters to customize service behavior.
+ """
+ super().__init__(**kwargs)
+
+ assert history_msg_count >= contextual_msg_max_count >= contextual_msg_min_count
+ self._history_msg_count: int = history_msg_count
+ self._contextual_msg_max_count: int = contextual_msg_max_count
+ self._contextual_msg_min_count: int = contextual_msg_min_count
+
+ self._message_lock = threading.Lock()
+
+ def add_messages_pair(self, messages: List[Message]):
+ """
+ Adds a list of messages to the chat history, it can be a pair [user_message, assistant_message].
+ Ensuring the message list remains sorted by creation time and does not exceed the maximum history message count.
+
+ Args:
+ messages (List[Message] | Message): A single message instance or a list of message instances
+ to be added to the chat history.
+ """
+ assert messages, "messages should not be empty!"
+
+ with self._message_lock:
+ # Append the sorted messages to the chat history
+ self._chat_messages.append(messages)
+
+ # Sort the messages by their creation time to maintain chronological order
+ self._chat_messages.sort(key=lambda x: x[0].time_created)
+
+ # If the chat history exceeds the allowed message count, remove the oldest messages
+ if len(self._chat_messages) > self._history_msg_count:
+ gap_size = len(self._chat_messages) - self._history_msg_count
+ for _ in range(gap_size):
+ self._chat_messages.pop(0)
+
+ for message in messages:
+ if message.role_name and message.role_name != self.assistant_name \
+ and message.role_name not in self._role_names:
+ self._role_names.append(message.role_name)
+
+ def register_operation(self, name: str, operation_config: dict, **kwargs):
+ if name in self._operation_dict:
+ self.logger.warning(f"op_name={name} is registered before!")
+ return
+
+ operation: BaseOperation = init_instance_by_config(
+ config=operation_config,
+ name=name,
+ user_name=self._assistant_name,
+ target_names=self._role_names,
+ chat_messages=self._chat_messages,
+ message_lock=self._message_lock,
+ memoryscope_context=self._context,
+ contextual_msg_max_count=self._contextual_msg_max_count,
+ contextual_msg_min_count=self._contextual_msg_min_count)
+
+ # Initialize workflow for each operation
+ operation.init_workflow(**kwargs)
+ self._operation_dict[name] = operation
+ self.logger.info(f"service={self.__class__.__name__} init operation={name}")
+
+ def init_service(self, **kwargs):
+ for name, operation_config in self._operations_conf.items():
+ self.register_operation(name, operation_config, **kwargs)
+
+ def run_operation(self, name: str, role_name: str = "", **kwargs):
+ """
+ Executes a specific operation by its name with provided keyword arguments.
+
+ Args:
+ name (str): The name of the operation to execute.
+ role_name (str): The name of the operation to execute.
+ **kwargs: Keyword arguments for the operation's execution.
+
+ Returns:
+ The result of the operation execution, if any. Otherwise, None.
+
+ Raises:
+ Warning: If the operation name is not initialized in `_operation_dict`.
+ """
+ if name not in self._operation_dict:
+ self.logger.warning(f"operation={name} is not registered!")
+ return
+
+ target_name = self._human_name
+ if role_name:
+ target_name = role_name
+ if role_name not in self._role_names:
+ self._role_names.append(role_name)
+
+ return self._operation_dict[name].run_operation(target_name=target_name, **kwargs)
+
+ def start_backend_service(self, name: str = None, **kwargs):
+ """
+ Start all backend operations.
+ """
+ for op_name, operation in self._operation_dict.items():
+ if name:
+ if op_name == name:
+ operation.start_operation_backend(**kwargs)
+ else:
+ if operation.operation_type == "backend":
+ operation.start_operation_backend(**kwargs)
+
+ def stop_backend_service(self, wait_service: bool = False):
+ """
+ Stops all backend operations that are currently running.
+ """
+ for _, operation in self._operation_dict.items():
+ if operation.operation_type == "backend":
+ operation.stop_operation_backend(wait_operation=wait_service)
diff --git a/memoryscope/memoryscope/core/storage/__init__.py b/memoryscope/memoryscope/core/storage/__init__.py
new file mode 100644
index 00000000..36893c7e
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/__init__.py
@@ -0,0 +1,23 @@
+from .base_memory_store import BaseMemoryStore
+from .base_monitor import BaseMonitor
+from .dummy_memory_store import DummyMemoryStore
+from .dummy_monitor import DummyMonitor
+from .llama_index_es_memory_store import LlamaIndexEsMemoryStore
+from .llama_index_sync_elasticsearch import (
+ # get_elasticsearch_client,
+ # _mode_must_match_retrieval_strategy,
+ # _to_elasticsearch_filter,
+ # _to_llama_similarities,
+ ESCombinedRetrieveStrategy,
+ SyncElasticsearchStore
+)
+
+__all__ = [
+ "BaseMemoryStore",
+ "BaseMonitor",
+ "DummyMemoryStore",
+ "DummyMonitor",
+ "LlamaIndexEsMemoryStore",
+ "ESCombinedRetrieveStrategy",
+ "SyncElasticsearchStore"
+]
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/storage/base_memory_store.py b/memoryscope/memoryscope/core/storage/base_memory_store.py
new file mode 100644
index 00000000..a06167ac
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/base_memory_store.py
@@ -0,0 +1,79 @@
+from abc import ABCMeta, abstractmethod
+from typing import Dict, List
+
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class BaseMemoryStore(metaclass=ABCMeta):
+ """
+ An abstract base class defining the interface for a memory store which handles memory nodes.
+ It outlines essential operations like retrieval, updating, flushing, and closing of memory scopes.
+ """
+
+ @abstractmethod
+ def retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] = None) -> List[MemoryNode]:
+ """
+ Retrieves a list of MemoryNode objects that are most relevant to the query,
+ considering a filter dictionary for additional constraints. The number of nodes returned
+ is limited by top_k.
+
+ Args:
+ query (str): The query string used to find relevant memories.
+ top_k (int): The maximum number of MemoryNode objects to return.
+ filter_dict (Dict[str, List[str]]): A dictionary with keys representing filter fields
+ and values as lists of strings for filtering criteria.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects sorted by relevance to the query,
+ limited to top_k items.
+ """
+ pass
+
+ @abstractmethod
+ async def a_retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] = None) -> List[MemoryNode]:
+ """
+ Asynchronously retrieves a list of MemoryNode objects that best match the query,
+ respecting a filter dictionary, with the result size capped at top_k.
+
+ Args:
+ query (str): The text to search for in memory nodes.
+ top_k (int): Maximum number of nodes to return.
+ filter_dict (Dict[str, List[str]]): Filters to apply on memory nodes.
+
+ Returns:
+ List[MemoryNode]: A list of up to top_k MemoryNode objects matching the criteria.
+ """
+ pass
+
+ @abstractmethod
+ def batch_insert(self, nodes: List[MemoryNode]):
+ pass
+
+ @abstractmethod
+ def batch_update(self, nodes: List[MemoryNode], update_embedding: bool = True):
+ pass
+
+ @abstractmethod
+ def batch_delete(self, nodes: List[MemoryNode]):
+ pass
+
+ def flush(self):
+ """
+ Flushes any pending memory updates or operations to ensure data consistency.
+ This method should be overridden by subclasses to provide the specific flushing mechanism.
+ """
+ pass
+
+ @abstractmethod
+ def close(self):
+ """
+ Closes the memory store, releasing any resources associated with it.
+ Subclasses must implement this method to define how the memory store is properly closed.
+ """
+ pass
diff --git a/memoryscope/memoryscope/core/storage/base_monitor.py b/memoryscope/memoryscope/core/storage/base_monitor.py
new file mode 100644
index 00000000..7a4521c0
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/base_monitor.py
@@ -0,0 +1,47 @@
+from abc import ABCMeta, abstractmethod
+
+
+class BaseMonitor(metaclass=ABCMeta):
+ """
+ An abstract base class defining the interface for monitor classes.
+ Subclasses should implement the methods defined here to provide concrete monitoring behavior.
+ """
+
+ def __init__(self, **kwargs):
+ pass
+
+ @abstractmethod
+ def add(self):
+ """
+ Abstract method to add data or events to the monitor.
+ This method should be implemented by subclasses to define how data is added into the monitoring system.
+
+ :return: None
+ """
+
+ @abstractmethod
+ def add_token(self):
+ """
+ Abstract method to add a token or a specific type of identifier to the monitor.
+ Subclasses should implement this to specify how tokens are managed within the monitoring context.
+
+ :return: None
+ """
+
+ def flush(self):
+ """
+ Method to flush any buffered data in the monitor.
+ Intended to ensure that all pending recorded data is processed or written out.
+
+ :return: None
+ """
+ pass
+
+ def close(self):
+ """
+ Method to close the monitor, performing necessary cleanup operations.
+ This could include releasing resources, closing files, or any other termination tasks.
+
+ :return: None
+ """
+ pass
diff --git a/memoryscope/memoryscope/core/storage/dummy_memory_store.py b/memoryscope/memoryscope/core/storage/dummy_memory_store.py
new file mode 100644
index 00000000..2d5eeede
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/dummy_memory_store.py
@@ -0,0 +1,48 @@
+from typing import Dict, List
+
+from memoryscope.core.models.base_model import BaseModel
+from memoryscope.core.storage.base_memory_store import BaseMemoryStore
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class DummyMemoryStore(BaseMemoryStore):
+ """
+ Placeholder implementation of a memory storage system interface. Defines methods for querying, updating,
+ and closing memory nodes with asynchronous capabilities, leveraging an embedding model for potential
+ semantic retrieval. Actual storage operations are not implemented.
+ """
+
+ def __init__(self, embedding_model: BaseModel, **kwargs):
+ """
+ Initializes the DummyMemoryStore with an embedding model and additional keyword arguments.
+
+ Args:
+ embedding_model (BaseModel): The model used to embed data for potential similarity-based retrieval.
+ **kwargs: Additional keyword arguments for configuration or future expansion.
+ """
+ self.embedding_model: BaseModel = embedding_model
+ self.kwargs = kwargs
+
+ def retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] = None) -> List[MemoryNode]:
+ pass
+
+ async def a_retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] = None) -> List[MemoryNode]:
+ pass
+
+ def batch_insert(self, nodes: List[MemoryNode]):
+ pass
+
+ def batch_update(self, nodes: List[MemoryNode], update_embedding: bool = True):
+ pass
+
+ def batch_delete(self, nodes: List[MemoryNode]):
+ pass
+
+ def close(self):
+ pass
diff --git a/memoryscope/memoryscope/core/storage/dummy_monitor.py b/memoryscope/memoryscope/core/storage/dummy_monitor.py
new file mode 100644
index 00000000..816202e5
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/dummy_monitor.py
@@ -0,0 +1,30 @@
+from memoryscope.core.storage.base_monitor import BaseMonitor
+
+
+class DummyMonitor(BaseMonitor):
+ """
+ DummyMonitor serves as a placeholder or mock class extending BaseMonitor,
+ providing empty method bodies for 'add', 'add_token', and 'close' operations.
+ This can be used for testing or in situations where a full monitor implementation is not required.
+ """
+
+ def add(self):
+ """
+ Placeholder for adding data to the monitor.
+ This method currently does nothing.
+ """
+ pass
+
+ def add_token(self):
+ """
+ Placeholder for adding a token to the monitored data.
+ This method currently does nothing.
+ """
+ pass
+
+ def close(self):
+ """
+ Placeholder for closing the monitor and performing any necessary cleanup.
+ This method currently does nothing.
+ """
+ pass
diff --git a/memoryscope/memoryscope/core/storage/llama_index_es_memory_store.py b/memoryscope/memoryscope/core/storage/llama_index_es_memory_store.py
new file mode 100644
index 00000000..b77523f4
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/llama_index_es_memory_store.py
@@ -0,0 +1,177 @@
+import random
+import pickle
+from typing import Dict, List
+
+from llama_index.core import VectorStoreIndex
+from llama_index.core.schema import TextNode, NodeWithScore, QueryBundle
+
+from memoryscope.core.models.base_model import BaseModel
+from memoryscope.core.storage.base_memory_store import BaseMemoryStore
+from memoryscope.core.storage.llama_index_sync_elasticsearch import (SyncElasticsearchStore,
+ ESCombinedRetrieveStrategy,
+ _to_elasticsearch_filter)
+from memoryscope.core.utils.logger import Logger
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class LlamaIndexEsMemoryStore(BaseMemoryStore):
+
+ def __init__(self,
+ embedding_model: BaseModel,
+ index_name: str,
+ es_url: str,
+ retrieve_mode: str = "dense",
+ hybrid_alpha: float = None,
+ **kwargs):
+ self.emb_dims = None
+ self.index_name = index_name
+ self.embedding_model: BaseModel = embedding_model
+ retrieval_strategy = ESCombinedRetrieveStrategy(retrieve_mode=retrieve_mode, hybrid_alpha=hybrid_alpha)
+ self.es_store = SyncElasticsearchStore(index_name=index_name,
+ es_url=es_url,
+ retrieval_strategy=retrieval_strategy,
+ **kwargs)
+
+ # TODO The llamaIndex utilizes some deprecated functions, hence langchain logs warning messages. By
+ # adding the following lines of code, the display of deprecated information is suppressed.
+ self.index = VectorStoreIndex.from_vector_store(vector_store=self.es_store,
+ embed_model=self.embedding_model.model)
+
+ self.logger = Logger.get_logger("es_memory_store")
+
+ def retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] | Dict[str, str] = None) -> List[MemoryNode]:
+ # if index is not created, return []
+ exists = self.es_store.client.indices.exists(index=self.index_name)
+ if not exists:
+ return []
+
+ if filter_dict is None:
+ filter_dict = {}
+
+ es_filter = _to_elasticsearch_filter(filter_dict)
+ retriever = self.index.as_retriever(vector_store_kwargs={"es_filter": es_filter, "fields": ['embedding']},
+ similarity_top_k=top_k,
+ sparse_top_k=top_k)
+
+ if query:
+ text_nodes = retriever.retrieve(query)
+ if text_nodes and text_nodes[0].embedding:
+ self.emb_dims = len(text_nodes[0].embedding)
+ else:
+ text_nodes = self.es_store.sync_search_all_with_filter(es_filter, ['embedding'])
+ self.logger.log_dictionary_info({
+ "action": "retrieve_memories",
+ "query": query,
+ "text_nodes": [f"ID: {n.node_id} |Text: {n.text}" for n in text_nodes]
+ })
+ return [self._text_node_2_memory_node(n) for n in text_nodes]
+
+ async def a_retrieve_memories(self,
+ query: str = "",
+ top_k: int = 3,
+ filter_dict: Dict[str, List[str]] | Dict[str, str] = None) -> List[MemoryNode]:
+ raise NotImplementedError
+
+ def batch_insert(self, nodes: List[MemoryNode]):
+ self.index.insert_nodes([self._memory_node_2_text_node(node) for node in nodes])
+
+ def batch_update(self, nodes: List[MemoryNode], update_embedding: bool = True):
+ if update_embedding:
+ for node in nodes:
+ node.vector = []
+
+ self.batch_delete(nodes)
+ self.batch_insert(nodes)
+
+ def batch_delete(self, nodes: List[MemoryNode]):
+ # TODO batch_delete
+ for node in nodes:
+ self.delete(node)
+
+ def insert(self, node: MemoryNode):
+ self.index.insert_nodes([self._memory_node_2_text_node(node)])
+ self.logger.log_dictionary_info({
+ "action": "insert",
+ "node": f"ID: {node.memory_id} | Text: {node.content} | Key: {node.key} | Type: {node.memory_type}"
+ })
+
+ def delete(self, node: MemoryNode):
+ self.logger.log_dictionary_info({
+ "action": "delete",
+ "id": node.memory_id,
+ })
+ return self.es_store.delete(node.memory_id)
+
+ def update(self, node: MemoryNode, update_embedding: bool = True):
+ if update_embedding:
+ node.vector = []
+ self.delete(node)
+ self.insert(node)
+
+ def close(self):
+ """
+ Closes the Elasticsearch store, releasing any resources associated with it.
+ """
+ self.es_store.close()
+
+ def dummy_query_vector(self):
+ random_floats = [random.uniform(0, 1) for _ in range(self.emb_dims)]
+ return random_floats
+
+ @staticmethod
+ def _memory_node_2_text_node(memory_node: MemoryNode) -> TextNode:
+ """
+ Converts a MemoryNode object into a TextNode object.
+
+ Args:
+ memory_node (MemoryNode): The MemoryNode to be converted.
+
+ Returns:
+ TextNode: The converted TextNode with content and metadata from the MemoryNode.
+ """
+ embedding = memory_node.vector
+ key_vector_str = pickle.dumps(memory_node.key_vector).decode('latin1')
+ if not embedding:
+ embedding = None
+ metadatas = memory_node.model_dump(exclude={"content",
+ "vector",
+ "key_vector",
+ "score_recall",
+ "score_rank",
+ "score_rerank"})
+ metadatas["key_vector"] = key_vector_str
+ return TextNode(id_=memory_node.memory_id,
+ text=memory_node.content,
+ embedding=embedding,
+ text_template="{content}",
+ metadata=metadatas)
+
+
+
+ @staticmethod
+ def _text_node_2_memory_node(text_node: NodeWithScore) -> MemoryNode:
+ """
+ Converts a NodeWithScore object into a MemoryNode object.
+
+ Args:
+ text_node (NodeWithScore): The NodeWithScore to be converted, typically retrieved from search results.
+
+ Returns:
+ MemoryNode: The converted MemoryNode with text and metadata from the NodeWithScore.
+ """
+
+ if text_node.metadata.get("key_vector", None):
+ key_vector = pickle.loads(text_node.metadata["key_vector"].encode('latin1'))
+ else:
+ key_vector = []
+ text_node.metadata["key_vector"] = key_vector
+
+ text_node.metadata["vector"] = text_node.embedding if text_node.embedding else []
+
+ if hasattr(text_node, "score"):
+ text_node.metadata["score_recall"] = text_node.score
+
+ return MemoryNode(content=text_node.text, **text_node.metadata)
diff --git a/memoryscope/memoryscope/core/storage/llama_index_sync_elasticsearch.py b/memoryscope/memoryscope/core/storage/llama_index_sync_elasticsearch.py
new file mode 100644
index 00000000..bcd675cc
--- /dev/null
+++ b/memoryscope/memoryscope/core/storage/llama_index_sync_elasticsearch.py
@@ -0,0 +1,795 @@
+"""Elasticsearch vector store."""
+
+from typing import Any, Callable, Dict, List, Literal, Optional, Union, cast
+
+import nest_asyncio
+import numpy as np
+from memoryscope.core.utils.logger import Logger
+from memoryscope.core.memoryscope_context import get_memoryscope_context
+
+from elasticsearch import AsyncElasticsearch, Elasticsearch
+from elasticsearch.helpers.vectorstore import (
+ AsyncBM25Strategy,
+ AsyncSparseVectorStrategy,
+ AsyncDenseVectorStrategy,
+ AsyncRetrievalStrategy,
+ DistanceMetric,
+)
+from elasticsearch.helpers.vectorstore import VectorStore
+from llama_index.core.bridge.pydantic import PrivateAttr
+from llama_index.core.schema import BaseNode, MetadataMode, TextNode
+from llama_index.core.vector_stores.types import (
+ BasePydanticVectorStore,
+ VectorStoreQuery,
+ VectorStoreQueryMode,
+ VectorStoreQueryResult,
+)
+from llama_index.core.vector_stores.utils import (
+ metadata_dict_to_node,
+ node_to_metadata_dict,
+)
+from llama_index.vector_stores.elasticsearch.utils import (
+ get_user_agent,
+)
+
+DISTANCE_STRATEGIES = Literal[
+ "COSINE",
+ "DOT_PRODUCT",
+ "EUCLIDEAN_DISTANCE",
+]
+
+
+def get_elasticsearch_client(
+ url: Optional[str] = None,
+ cloud_id: Optional[str] = None,
+ api_key: Optional[str] = None,
+ username: Optional[str] = None,
+ password: Optional[str] = None,
+ use_async: Optional[bool] = False,
+) -> AsyncElasticsearch:
+ if url and cloud_id:
+ raise ValueError(
+ "Both es_url and cloud_id are defined. Please provide only one."
+ )
+
+ connection_params: Dict[str, Any] = {}
+
+ if url:
+ connection_params["hosts"] = [url]
+ elif cloud_id:
+ connection_params["cloud_id"] = cloud_id
+ else:
+ raise ValueError("Please provide either elasticsearch_url or cloud_id.")
+
+ if api_key:
+ connection_params["api_key"] = api_key
+ elif username and password:
+ connection_params["basic_auth"] = (username, password)
+ if use_async:
+ es_client = AsyncElasticsearch(
+ **connection_params, headers={"user-agent": get_user_agent()}
+ )
+ else:
+ es_client = Elasticsearch(
+ **connection_params, headers={"user-agent": get_user_agent()}
+ )
+
+ es_client.info() # use sync client so don't have to 'await' to just get info
+
+ return es_client
+
+
+def _to_llama_similarities(scores: List[float]) -> List[float]:
+ """
+ Converts a list of similarity scores into a normalized form for LlamaIndex compatibility.
+ The normalization involves an exponential transformation based on the maximum score in the list.
+
+ Args:
+ scores (List[float]): A list of raw similarity scores.
+
+ Returns:
+ List[float]: A list of normalized similarity scores suitable for LlamaIndex.
+ """
+ if scores is None or len(scores) == 0:
+ return []
+
+ scores_to_norm: np.ndarray = np.array(scores)
+ # Normalize scores by subtracting the max score and applying the exponential function
+ return np.exp(scores_to_norm - np.max(scores_to_norm)).tolist()
+
+
+def _mode_must_match_retrieval_strategy(
+ mode: VectorStoreQueryMode, retrieval_strategy: AsyncRetrievalStrategy
+) -> None:
+ """
+ Different retrieval strategies require different ways of indexing that must be known at the
+ time of adding data. The query mode is known at query time. This function checks if the
+ retrieval strategy (and way of indexing) is compatible with the query mode and raises and
+ exception in the case of a mismatch.
+ """
+ if mode == VectorStoreQueryMode.DEFAULT:
+ # it's fine to not specify an explicit other mode
+ return
+
+ mode_retrieval_dict = {
+ VectorStoreQueryMode.SPARSE: AsyncSparseVectorStrategy,
+ VectorStoreQueryMode.TEXT_SEARCH: AsyncBM25Strategy,
+ VectorStoreQueryMode.HYBRID: AsyncDenseVectorStrategy,
+ }
+
+ required_strategy = mode_retrieval_dict.get(mode)
+ if not required_strategy:
+ raise NotImplementedError(f"query mode {mode} currently not supported")
+
+ if not isinstance(retrieval_strategy, required_strategy):
+ raise ValueError(
+ f"query mode {mode} incompatible with retrieval strategy {type(retrieval_strategy)}, "
+ f"expected {required_strategy}"
+ )
+
+ if mode == VectorStoreQueryMode.HYBRID and not retrieval_strategy.hybrid:
+ raise ValueError(f"to enable hybrid mode, it must be set in retrieval strategy")
+
+
+class ESCombinedRetrieveStrategy(AsyncDenseVectorStrategy):
+
+ def __init__(
+ self,
+ *,
+ distance: DistanceMetric = DistanceMetric.COSINE,
+ model_id: Optional[str] = None,
+ retrieve_mode: str = "dense",
+ rrf: Union[bool, Dict[str, Any]] = True,
+ text_field: Optional[str] = "text_field",
+ hybrid_alpha: Optional[float] = None,
+ ):
+ if retrieve_mode == "dense":
+ self.alpha = 1.0
+ elif retrieve_mode == "sparse":
+ # self.alpha = 0.0
+ raise NotImplementedError
+ elif retrieve_mode == "hybrid":
+ # self.alpha = hybrid_alpha
+ raise NotImplementedError
+
+ super().__init__(distance=distance, model_id=model_id, hybrid=True, rrf=rrf, text_field=text_field)
+
+ def _hybrid(self, query: str, knn: Dict[str, Any], filter: List[Dict[str, Any]], top_k: int) -> Dict[str, Any]:
+ # Add a query to the knn query.
+ # RRF is used to even the score from the knn query and text query
+ # RRF has two optional parameters: {'rank_constant':int, 'window_size':int}
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html
+ if not query:
+ query_body = {
+ "query": {
+ "bool": {
+ "filter": filter,
+ }
+ },
+ }
+ else:
+ query_body = {
+ "knn": knn,
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "match": {
+ self.text_field: {
+ "query": query,
+ "boost": (1 - self.alpha) if self.alpha is not None else 1.0,
+ }
+ },
+ }
+ ],
+ "filter": filter,
+ },
+ },
+ }
+
+ if self.alpha is None and isinstance(self.rrf, Dict):
+ query_body["rank"] = {"rrf": self.rrf}
+ elif self.alpha is None and isinstance(self.rrf, bool) and self.rrf is True:
+ query_body["rank"] = {"rrf": {"window_size": top_k}}
+
+ return query_body
+
+ def es_query(
+ self,
+ *,
+ query: Optional[str],
+ query_vector: Optional[List[float]],
+ text_field: str,
+ vector_field: str,
+ k: int,
+ num_candidates: int,
+ filter: List[Dict[str, Any]] = None,
+ ) -> Dict[str, Any]:
+ if filter is None:
+ filter = []
+
+ knn = {
+ "filter": filter,
+ "field": vector_field,
+ "k": k,
+ "num_candidates": num_candidates,
+ "boost": self.alpha if self.alpha is not None else 1.0,
+ }
+
+ if query_vector is not None:
+ knn["query_vector"] = query_vector
+ else:
+ # Inference in Elasticsearch. When initializing we make sure to always have
+ # a model_id if we don't have an embedding_service.
+ knn["query_vector_builder"] = {
+ "text_embedding": {
+ "model_id": self.model_id,
+ "model_text": query,
+ }
+ }
+
+ if self.hybrid:
+ return self._hybrid(query=cast(str, query), knn=knn, filter=filter, top_k=k)
+
+ return {"knn": knn}
+
+
+ def before_index_creation(
+ self, *, client: AsyncElasticsearch, text_field: str, vector_field: str
+ ) -> None:
+ if self.model_id:
+ from elasticsearch.helpers.vectorstore._async._utils import model_must_be_deployed
+ import asyncio
+ print('before_index_creation')
+ asyncio.run(model_must_be_deployed(client, self.model_id))
+ print('before_index_creation 2')
+
+
+def _to_elasticsearch_filter(standard_filters: Dict[str, List[str]]) -> Dict[str, Any]:
+ """
+ Converts standard Llama-index filters into a format compatible with Elasticsearch.
+
+ This function transforms dictionary-based filters, where each key represents a field and
+ the value is a list of strings, into an Elasticsearch query structure. It supports both
+ list values (interpreted as 'should' clauses for OR logic) and single values (interpreted
+ as 'must' clauses for AND logic).
+
+ Args:
+ standard_filters (Dict[str, List[str]]): A dictionary containing filter criteria,
+ where keys are field names and values are lists of strings or single string values
+ representing filter values.
+
+ Returns:
+ Dict[str, Any]: A dictionary structured as an Elasticsearch filter query.
+ """
+ result = {
+ "bool": {}
+ }
+ for key, value in standard_filters.items():
+ if isinstance(value, list):
+ operands = []
+ for v in value:
+ key_str = f"metadata.{key}.keyword" if isinstance(v, str) else f"metadata.{key}"
+ operands.append(
+ {
+ "term":
+ {
+ key_str: {"value": v}
+ }
+ }
+ )
+ result['bool'].update({"should": operands}) # Add 'should' clause for OR logic
+ result['bool'].update({"minimum_should_match": 1}) # Ensure at least one 'should' match
+ else:
+ key_str = f"metadata.{key}.keyword" if isinstance(value, str) else f"metadata.{key}"
+ operand = [{
+ "term": {
+ key_str: {
+ "value": value,
+ }
+ }
+ }]
+ if "must" in result['bool']:
+ result['bool']['must'].extend(operand) # Extend existing 'must' clause for AND logic
+ else:
+ result['bool'].update({"must": operand}) # Initialize 'must' clause if not present
+ return result
+
+
+class SyncElasticsearchStore(BasePydanticVectorStore):
+ """
+ Elasticsearch vector store.
+
+ Args:
+ index_name: Name of the Elasticsearch index.
+ es_client: Optional. Pre-existing AsyncElasticsearch client.
+ es_url: Optional. Elasticsearch URL.
+ es_cloud_id: Optional. Elasticsearch cloud ID.
+ es_api_key: Optional. Elasticsearch API key.
+ es_user: Optional. Elasticsearch username.
+ es_password: Optional. Elasticsearch password.
+ text_field: Optional. Name of the Elasticsearch field that stores the text.
+ vector_field: Optional. Name of the Elasticsearch field that stores the
+ embedding.
+ batch_size: Optional. Batch size for bulk indexing. Defaults to 200.
+ distance_strategy: Optional. Distance strategy to use for similarity search.
+ Defaults to "COSINE".
+ retrieval_strategy: Retrieval strategy to use. AsyncBM25Strategy /
+ AsyncSparseVectorStrategy / AsyncDenseVectorStrategy / AsyncRetrievalStrategy.
+ Defaults to AsyncDenseVectorStrategy.
+
+ Raises:
+ ConnectionError: If AsyncElasticsearch client cannot connect to Elasticsearch.
+ ValueError: If neither es_client nor es_url nor es_cloud_id is provided.
+
+ Examples:
+ `pip install llama-index-vector-stores-elasticsearch`
+
+ ```python
+ from llama_index.vector_stores import ElasticsearchStore
+
+ # Additional setup for ElasticsearchStore class
+ index_name = "my_index"
+ es_url = "http://localhost:9200"
+ es_cloud_id = "" # Found within the deployment page
+ es_user = "elastic"
+ es_password = "" # Provided when creating deployment or can be reset
+ es_api_key = "" # Create an API key within Kibana (Security -> API Keys)
+
+ # Connecting to ElasticsearchStore locally
+ es_local = ElasticsearchStore(
+ index_name=index_name,
+ es_url=es_url)
+
+ # Connecting to Elastic Cloud with username and password
+ es_cloud_user_pass = ElasticsearchStore(
+ index_name=index_name,
+ es_cloud_id=es_cloud_id,
+ es_user=es_user,
+ es_password=es_password)
+
+ # Connecting to Elastic Cloud with API Key
+ es_cloud_api_key = ElasticsearchStore(
+ index_name=index_name,
+ es_cloud_id=es_cloud_id,
+ es_api_key=es_api_key,
+ )
+ ```
+
+ """
+
+ class Config:
+ # allow pydantic to tolarate its inability to validate AsyncRetrievalStrategy
+ arbitrary_types_allowed = True
+
+ stores_text: bool = True
+ index_name: str
+ es_client: Optional[Any]
+ es_url: Optional[str]
+ es_cloud_id: Optional[str]
+ es_api_key: Optional[str]
+ es_user: Optional[str]
+ es_password: Optional[str]
+ text_field: str = "content"
+ vector_field: str = "embedding"
+ batch_size: int = 200
+ distance_strategy: Optional[DISTANCE_STRATEGIES] = "COSINE"
+ retrieval_strategy: AsyncRetrievalStrategy
+ logger: Logger = None
+ log_elasticsearch_dynamic: bool = False
+
+ _store = PrivateAttr()
+
+ def __init__(
+ self,
+ index_name: str,
+ es_client: Optional[Any] = None,
+ es_url: Optional[str] = None,
+ es_cloud_id: Optional[str] = None,
+ es_api_key: Optional[str] = None,
+ es_user: Optional[str] = None,
+ es_password: Optional[str] = None,
+ text_field: str = "content",
+ vector_field: str = "embedding",
+ batch_size: int = 200,
+ distance_strategy: Optional[DISTANCE_STRATEGIES] = "COSINE",
+ retrieval_strategy: Optional[AsyncRetrievalStrategy] = None,
+ ) -> None:
+ nest_asyncio.apply()
+
+ if not es_client:
+ es_client = get_elasticsearch_client(
+ url=es_url,
+ cloud_id=es_cloud_id,
+ api_key=es_api_key,
+ username=es_user,
+ password=es_password,
+ )
+
+ if retrieval_strategy is None:
+ retrieval_strategy = AsyncDenseVectorStrategy(
+ distance=DistanceMetric[distance_strategy]
+ )
+
+ metadata_mappings = {
+ "document_id": {"type": "keyword"},
+ "doc_id": {"type": "keyword"},
+ "ref_doc_id": {"type": "keyword"},
+ }
+
+ self._store = VectorStore(
+ user_agent=get_user_agent(),
+ client=es_client,
+ index=index_name,
+ retrieval_strategy=retrieval_strategy,
+ text_field=text_field,
+ vector_field=vector_field,
+ metadata_mappings=metadata_mappings,
+ )
+
+ super().__init__(
+ index_name=index_name,
+ es_client=es_client,
+ es_url=es_url,
+ es_cloud_id=es_cloud_id,
+ es_api_key=es_api_key,
+ es_user=es_user,
+ es_password=es_password,
+ text_field=text_field,
+ vector_field=vector_field,
+ batch_size=batch_size,
+ distance_strategy=distance_strategy,
+ retrieval_strategy=retrieval_strategy,
+ )
+
+ self.logger = Logger.get_logger("elastic_search")
+ self.log_elasticsearch_dynamic = get_memoryscope_context().log_elasticsearch_dynamic
+
+ @property
+ def client(self) -> Any:
+ """
+ Get the asynchronous Elasticsearch client.
+
+ Returns:
+ Any: The asynchronous Elasticsearch client instance configured for this store.
+ """
+ return self._store.client
+
+ def close(self) -> None:
+ return self._store.close()
+
+ def add(
+ self,
+ nodes: List[BaseNode],
+ *,
+ create_index_if_not_exists: bool = True,
+ **add_kwargs: Any,
+ ) -> List[str]:
+ """
+ Adds a list of nodes, each containing embeddings, to an Elasticsearch index.
+ Optionally creates the index if it does not already exist.
+
+ Args:
+ nodes (List[BaseNode]): A list of node objects, each encapsulating an embedding.
+ create_index_if_not_exists (bool, optional):
+ A flag indicating whether to create the Elasticsearch index if it's not present.
+ Defaults to True.
+
+ Returns:
+ List[str]: A list of node IDs that have been successfully added to the index.
+
+ Raises:
+ ImportError: If the 'elasticsearch[async]' Python package is not installed.
+ BulkIndexError: If there is a failure during the asynchronous bulk indexing with AsyncElasticsearch.
+
+ Note:
+ This method delegates the actual operation to the `sync_add` method.
+ """
+ add_res = self.sync_add(nodes, create_index_if_not_exists=create_index_if_not_exists)
+ self.log_vector_store_brief(title='after add')
+ return add_res
+
+
+ def sync_add(
+ self,
+ nodes: List[BaseNode],
+ *,
+ create_index_if_not_exists: bool = True,
+ **add_kwargs: Any,
+ ) -> List[str]:
+ """
+ Asynchronously adds a list of nodes, each containing an embedding, to the Elasticsearch index.
+
+ This method processes each node to extract its ID, embedding, text content, and metadata,
+ preparing them for batch insertion into the index. It ensures the index is created if not present
+ and respects the dimensionality of the embeddings for consistency.
+
+ Args:
+ nodes (List[BaseNode]): A list of node objects, each encapsulating an embedding.
+ create_index_if_not_exists (bool, optional): A flag indicating whether to create the Elasticsearch
+ index if it does not already exist. Defaults to True.
+ **add_kwargs (Any): Additional keyword arguments passed to the underlying add_texts method
+ for customization during the indexing process.
+
+ Returns:
+ List[str]: A list of node IDs that were successfully added to the index.
+
+ Raises:
+ ImportError: If the Elasticsearch Python client is not installed.
+ BulkIndexError: If there's a failure during the asynchronous bulk indexing operation.
+ """
+ if len(nodes) == 0:
+ return []
+
+ # Extract necessary components from each node
+ embeddings: List[List[float]] = [] # Embedding vectors
+ texts: List[str] = [] # Textual contents of nodes
+ metadatas: List[dict] = [] # Metadata associated with nodes
+ ids: List[str] = [] # Unique identifiers for nodes
+
+ for node in nodes:
+ ids.append(node.node_id) # Node identifier
+ embeddings.append(node.get_embedding()) # Node's embedding vector
+ texts.append(node.get_content(metadata_mode=MetadataMode.NONE)) # Node's raw text content
+ metadatas.append(node_to_metadata_dict(node, remove_text=True)) # Convert node to metadata dictionary
+
+ # Initialize the number of dimensions in the store if not set
+ if not self._store.num_dimensions:
+ self._store.num_dimensions = len(embeddings[0]) # Set based on the first node's embedding size
+
+ # Add the prepared data to the Elasticsearch index asynchronously
+ return self._store.add_texts(
+ texts=texts,
+ metadatas=metadatas,
+ vectors=embeddings,
+ ids=ids,
+ create_index_if_not_exists=create_index_if_not_exists,
+ bulk_kwargs=add_kwargs,
+ )
+
+ def delete(self, ref_doc_id: str, **delete_kwargs: Any) -> None:
+ """
+ Deletes a node from the Elasticsearch index using the provided reference document ID.
+
+ Optionally, extra keyword arguments can be supplied to customize the deletion behavior,
+ which are passed directly to Elasticsearch's `delete_by_query` operation.
+
+ Args:
+ ref_doc_id (str): The unique identifier of the node/document to be deleted.
+ delete_kwargs (Any): Additional keyword arguments for Elasticsearch's
+ `delete_by_query`. These might include query filters,
+ timeouts, or other operational configurations.
+
+ Raises:
+ Exception: If the deletion operation via Elasticsearch's `delete_by_query` fails.
+
+ Note:
+ This method internally calls a synchronous delete method (`sync_delete`)
+ to execute the deletion operation against Elasticsearch.
+ """
+ del_res = self.sync_delete(ref_doc_id, **delete_kwargs)
+ self.log_vector_store_brief(title='after delete')
+ return del_res
+
+ def sync_delete(self, ref_doc_id: str, **delete_kwargs: Any) -> None:
+ """
+ Synchronously deletes a node from the Elasticsearch index based on the reference document ID.
+
+ Args:
+ ref_doc_id (str): The unique identifier of the node/document to be deleted.
+ delete_kwargs (Any): Optional keyword arguments to be passed
+ to the delete_by_query operation of AsyncElasticsearch,
+ allowing for additional customization of the deletion process.
+
+ Raises:
+ Exception: If the deletion operation via AsyncElasticsearch's delete_by_query fails.
+
+ Note:
+ The function directly uses '_id' field to match the document for deletion instead of 'metadata.ref_doc_id',
+ ensuring targeted removal based on the document's unique identifier within Elasticsearch.
+ """
+ # The original commented line suggests an alternative query using 'metadata.ref_doc_id',
+ # but the active code line performs the deletion based on '_id', which typically aligns with 'ref_doc_id'.
+ return self._store.delete(query={"term": {"_id": ref_doc_id}}, **delete_kwargs)
+
+ def query(
+ self,
+ query: VectorStoreQuery,
+ custom_query: Optional[
+ Callable[[Dict, Union[VectorStoreQuery, None]], Dict]
+ ] = None,
+ es_filter: Optional[List[Dict]] = None,
+ **kwargs: Any,
+ ) -> VectorStoreQueryResult:
+ """
+ Executes a query against the Elasticsearch index to retrieve the top k most similar nodes
+ based on the input query embedding. Supports customization of the query process and
+ application of Elasticsearch filters.
+
+ Args:
+ query (VectorStoreQuery): The query containing the embedding and other parameters.
+ custom_query (Callable[[Dict, Union[VectorStoreQuery, None]], Dict], optional):
+ An optional custom function to modify the Elasticsearch query body, allowing for
+ additional query parameters or logic. Defaults to None.
+ es_filter (Optional[List[Dict]], optional): An optional Elasticsearch filter list to
+ apply to the query. If a filter is directly included in the `query`, this argument
+ will not be used. Defaults to None.
+ **kwargs (Any): Additional keyword arguments that might be used in the query process.
+
+ Returns:
+ VectorStoreQueryResult: The result of the query operation, including the most similar nodes.
+
+ Raises:
+ Exception: If an error occurs during the Elasticsearch query execution.
+
+ """
+
+ q_res = self.sync_query(query, custom_query, es_filter, **kwargs)
+ self.logger.log_dictionary_info({
+ "action": "query",
+ "query": query.query_str,
+ "result": [tn.text for tn in q_res.nodes]
+ })
+ return q_res
+
+ def sync_delete_all(self):
+ try:
+ self._store.client.delete_by_query(index=[self.index_name], body={"query": {"match_all": {}}})
+ except: # elasticsearch.NotFoundError
+ pass
+
+ def sync_search_all(self):
+ search_res = self._store.client.search(index=[self.index_name], body={"query": {"match_all": {}}})
+ return search_res
+
+ def log_vector_store_brief(self, title="current vector store content"):
+ if not self.log_elasticsearch_dynamic:
+ return "Dynamic elasticsearch logging is disabled to enhance performance."
+ search_res = self.sync_search_all()
+
+ brief = {
+ f"{hit['_source']['metadata']['memory_id']}({hit['_source']['metadata']['user_name']}/{hit['_source']['metadata']['target_name']}/{hit['_source']['metadata']['memory_type']})":
+ hit['_source']['content']
+ for hit in search_res["hits"]["hits"]
+ }
+ self.logger.log_dictionary_info(brief, title=title)
+
+ return brief
+
+ def sync_search_all_with_filter(self, es_filter, fields):
+ query_body = {'query': {'bool': {'filter': es_filter}}}
+ k = 1000
+ fields = ['embedding', 'metadata', 'content']
+ response = self.client.search(
+ index=self.index_name,
+ **query_body,
+ size=k,
+ source=True,
+ source_includes=fields,
+ )
+ res = []
+ for hit in response["hits"]["hits"]:
+ tn = TextNode(
+ id_=hit['_id'],
+ text=hit['_source']['content'],
+ embedding=hit['_source']['embedding'],
+ text_template="{content}",
+ metadata=hit['_source']['metadata']
+ )
+ res.append(
+ tn
+ )
+ return res
+
+ def sync_query(
+ self,
+ query: VectorStoreQuery,
+ custom_query: Optional[
+ Callable[[Dict, Union[VectorStoreQuery, None]], Dict]
+ ] = None,
+ es_filter: Optional[List[Dict]] = None,
+ fields: List[str] = [],
+ ) -> VectorStoreQueryResult:
+ """
+ Asynchronously queries the Elasticsearch index for the top k most similar nodes
+ based on the provided query embedding. Supports custom query modifications
+ and application of Elasticsearch filters.
+
+ Args:
+ query (VectorStoreQuery): The query containing the embedding and other details.
+ custom_query (Callable[[Dict, Union[VectorStoreQuery, None]], Dict], optional):
+ A custom function to modify the Elasticsearch query body. Defaults to None.
+ es_filter (List[Dict], optional): Additional filters to apply during the query.
+ If filters are present in the query, these filters will not be used. Defaults to None.
+ fields (List[str], optional): .
+
+ Returns:
+ VectorStoreQueryResult: The result of the query, including nodes, their IDs,
+ and similarity scores.
+
+ Raises:
+ Exception: If the Elasticsearch query encounters an error.
+
+ Note:
+ The mode of the query must align with the retrieval strategy set for this store.
+ In case of legacy metadata, a warning is logged and nodes are constructed accordingly.
+ """
+ _mode_must_match_retrieval_strategy(query.mode, self.retrieval_strategy)
+
+ if query.filters is not None and len(query.filters.legacy_filters()) > 0:
+ filter = [_to_elasticsearch_filter(query.filters)]
+ else:
+ filter = es_filter or []
+ num_candidates = query.similarity_top_k * 10 if query.similarity_top_k <= 1000 else query.similarity_top_k
+
+ hits = self._store.search(
+ query=query.query_str,
+ query_vector=query.query_embedding,
+ k=query.similarity_top_k,
+ num_candidates=num_candidates, # query.similarity_top_k * 10,
+ filter=filter,
+ custom_query=custom_query,
+ fields=fields,
+ )
+
+ return self.post_process_hits(hits)
+
+
+ def post_process_hits(self, hits: List[Dict[str, Any]]) -> VectorStoreQueryResult:
+ top_k_nodes = []
+ top_k_ids = []
+ top_k_scores = []
+
+ for hit in hits:
+ source = hit["_source"]
+ metadata = source.get("metadata", None)
+ embedding = source.get("embedding", None)
+ text = source.get(self.text_field, None)
+ node_id = hit["_id"]
+
+ try:
+ # Attempt to parse metadata using the standard method
+ node = metadata_dict_to_node(metadata)
+ node.text = text
+ node.embedding = embedding
+ except Exception:
+ # Legacy support for old metadata format
+ self.logger.warning(
+ f"Could not parse metadata from hit {hit['_source']['metadata']}"
+ )
+ node_info = source.get("node_info")
+ relationships = source.get("relationships", {})
+ start_char_idx = None
+ end_char_idx = None
+ if isinstance(node_info, dict):
+ start_char_idx = node_info.get("start", None)
+ end_char_idx = node_info.get("end", None)
+
+ node = TextNode(
+ text=text,
+ metadata=metadata,
+ id_=node_id,
+ embedding=embedding,
+ start_char_idx=start_char_idx,
+ end_char_idx=end_char_idx,
+ relationships=relationships,
+ )
+ top_k_nodes.append(node)
+ top_k_ids.append(node_id)
+ top_k_scores.append(hit.get("_rank", hit["_score"]))
+
+ if (
+ isinstance(self.retrieval_strategy, AsyncDenseVectorStrategy)
+ and self.retrieval_strategy.hybrid
+ ):
+ # total_rank = sum(top_k_scores)
+ top_k_scores = [rank for rank in top_k_scores]
+ # top_k_scores = [(total_rank - rank) / total_rank for rank in top_k_scores]
+ # top_k_scores = [total_rank - rank / total_rank for rank in top_k_scores]
+
+ return VectorStoreQueryResult(
+ nodes=top_k_nodes,
+ ids=top_k_ids,
+ # similarities=_to_llama_similarities(top_k_scores),
+ similarities=top_k_scores
+ )
diff --git a/memoryscope/memoryscope/core/utils/__init__.py b/memoryscope/memoryscope/core/utils/__init__.py
new file mode 100644
index 00000000..903b05a3
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/__init__.py
@@ -0,0 +1,33 @@
+from .datetime_handler import DatetimeHandler
+from .logger import Logger
+from .prompt_handler import PromptHandler
+from .registry import Registry
+from .response_text_parser import ResponseTextParser
+from .timer import Timer
+from .tool_functions import (
+ underscore_to_camelcase,
+ camelcase_to_underscore,
+ init_instance_by_config,
+ prompt_to_msg,
+ char_logo,
+ md5_hash,
+ contains_keyword,
+ cosine_similarity
+)
+
+__all__ = [
+ "DatetimeHandler",
+ "Logger",
+ "PromptHandler",
+ "Registry",
+ "ResponseTextParser",
+ "Timer",
+ "underscore_to_camelcase",
+ "camelcase_to_underscore",
+ "init_instance_by_config",
+ "prompt_to_msg",
+ "char_logo",
+ "md5_hash",
+ "contains_keyword",
+ "cosine_similarity"
+]
diff --git a/memoryscope/memoryscope/core/utils/datetime_handler.py b/memoryscope/memoryscope/core/utils/datetime_handler.py
new file mode 100644
index 00000000..29189277
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/datetime_handler.py
@@ -0,0 +1,318 @@
+import datetime
+import re
+from typing import List
+
+from memoryscope.constants.language_constants import WEEKDAYS, DATATIME_WORD_LIST, MONTH_DICT
+from memoryscope.enumeration.language_enum import LanguageEnum
+
+
+class DatetimeHandler(object):
+ """
+ Handles operations related to datetime such as parsing, extraction, and formatting,
+ with support for both Chinese and English contexts including weekday names and
+ specialized text parsing for date components.
+ """
+
+
+ def __init__(self, dt: datetime.datetime | str | int | float = None):
+ """
+ Initialize the DatetimeHandler instance with a datetime object, string, integer, or float representation
+ of a timestamp. If no argument is provided, the current time is used.
+
+ Args:
+ dt (datetime.datetime | str | int | float, optional):
+ The datetime to be handled. Can be a datetime object, a timestamp string, or a numeric timestamp.
+ Defaults to None, which sets the instance to the current datetime.
+
+ Attributes:
+ self._dt (datetime.datetime): The internal datetime representation of the input.
+ self._dt_info_dict (dict | None): A dictionary containing parsed datetime information, defaults to None.
+ """
+ if isinstance(dt, str | int | float):
+ if isinstance(dt, str):
+ dt = float(dt)
+ self._dt: datetime.datetime = datetime.datetime.fromtimestamp(dt)
+ elif isinstance(dt, datetime.datetime):
+ self._dt: datetime.datetime = dt
+ else:
+ self._dt: datetime.datetime = datetime.datetime.now()
+
+ self._dt_info_dict: dict | None = None
+
+ def _parse_dt_info(self, language: LanguageEnum):
+ """
+ Parses the datetime object (_dt) into a dictionary containing detailed date and time components,
+ including language-specific weekday representation.
+
+ Returns:
+ dict: A dictionary with keys representing date and time parts such as 'year', 'month',
+ 'day', 'hour', 'minute', 'second', 'week', and 'weekday' with respective values.
+ The 'weekday' value is translated based on the current language context.
+ """
+ return {
+ "year": self._dt.year,
+ "month": MONTH_DICT[language][self._dt.month - 1],
+ "day": self._dt.day,
+ "hour": self._dt.hour,
+ "minute": self._dt.minute,
+ "second": self._dt.second,
+ "week": self._dt.isocalendar().week,
+ "weekday": WEEKDAYS[language][self._dt.isocalendar().weekday - 1],
+ }
+
+ def get_dt_info_dict(self, language: LanguageEnum):
+ """
+ Property method to get the dictionary containing parsed datetime information.
+ If None, initialize using `_parse_dt_info`.
+
+ Returns:
+ dict: A dictionary with parsed datetime information.
+ """
+ if self._dt_info_dict is None:
+ self._dt_info_dict = self._parse_dt_info(language=language)
+ return self._dt_info_dict
+
+ @classmethod
+ def extract_date_parts_cn(cls, input_string: str) -> dict:
+ """
+ Extracts various components of a date (year, month, day, etc.) from an input string based on Chinese formats.
+
+ This method identifies year, month, day, weekday, and hour components within the input
+ string based on predefined patterns. It supports relative terms like '每' (every) and
+ translates weekday names into numeric representations.
+
+ Args:
+ input_string (str): The Chinese text containing date and time information.
+
+ Returns:
+ dict: A dictionary with keys 'year', 'month', 'day', 'weekday', and 'hour',
+ each holding the corresponding extracted value. If a component is not found,
+ it will not be included in the dictionary. For relative terms like '每' (every),
+ the value is set to -1.
+
+ """
+ # Extending our pattern to handle every/每 as a possible value.
+ patterns = {
+ "year": r"(\d+|每)年",
+ "month": r"(\d+|每)月",
+ "day": r"(\d+|每)日",
+ "weekday": r"周([一二三四五六日])",
+ "hour": r"(\d+)点"
+ }
+ weekday_dict = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7}
+ extracted_data = {}
+
+ # Search for patterns in the input string and populate the dictionary
+ for key, pattern in patterns.items():
+ match = re.search(pattern, input_string)
+ if match: # If there is a match, include it in the output dictionary
+ if match.group(1) == "每":
+ extracted_data[key] = -1
+ elif match.group(1) in weekday_dict.keys():
+ extracted_data[key] = weekday_dict[match.group(1)]
+ else:
+ extracted_data[key] = int(match.group(1))
+ return extracted_data
+
+ @classmethod
+ def extract_date_parts_en(cls, input_string: str) -> dict:
+ """
+ Extracts various components of a date (year, month, day, etc.) from an input string based on English formats.
+
+ This method employs regex patterns to identify and parse different date and time elements within the provided
+ text. It supports extraction of year, month name, day, 12-hour and 24-hour time formats, and weekdays.
+
+ Args:
+ input_string (str): The English text containing date and time information.
+
+ Returns:
+ dict: A dictionary containing the extracted date parts with default values of -1 where components are not
+ found. Keys include 'year', 'month', 'day', 'hour', 'minute', 'second', and 'weekday'.
+ """
+ date_info = {
+ "year": -1,
+ "month": -1,
+ "day": -1,
+ "hour": -1,
+ "minute": -1,
+ "second": -1,
+ "weekday": -1
+ }
+
+ # Patterns to extract the parts of the date/time
+ patterns = {
+ "year": r"\b(\d{4})\b",
+ "month": r"\b(January|February|March|April|May|June|July|August|September|October|November|December)\b",
+ "day_month_year": r"\b(?PJanuary|February|March|April|May|June|July|August|September|October"
+ r"|November|December) (?P\d{1,2}),? (?P\d{4})\b",
+ "day_month": r"\b(?PJanuary|February|March|April|May|June|July|August|September|October|November"
+ r"|December) (?P\d{1,2})\b",
+ "hour_12": r"\b(\d{1,2})\s*(AM|PM|am|pm)\b",
+ "hour_24": r"\b(\d{1,2}):(\d{2}):(\d{2})\b"
+ }
+
+ month_mapping = {
+ "January": 1, "February": 2, "March": 3, "April": 4, "May": 5, "June": 6, "July": 7, "August": 8,
+ "September": 9, "October": 10, "November": 11, "December": 12
+ }
+
+ weekday_mapping = {
+ "Monday": 1, "Tuesday": 2, "Wednesday": 3, "Thursday": 4, "Friday": 5, "Saturday": 6, "Sunday": 7
+ }
+
+ # Attempt to match full date (day month year)
+ day_month_year_match = re.search(patterns["day_month_year"], input_string)
+ if day_month_year_match:
+ date_info["year"] = int(day_month_year_match.group("year"))
+ date_info["month"] = month_mapping[day_month_year_match.group("month")]
+ date_info["day"] = int(day_month_year_match.group("day"))
+
+ # If year wasn't found, try matching day and month without year
+ elif date_info["year"] == -1:
+ day_month_match = re.search(patterns["day_month"], input_string)
+ if day_month_match:
+ date_info["month"] = month_mapping[day_month_match.group("month")]
+ date_info["day"] = int(day_month_match.group("day"))
+
+ # Extract year if not already found
+ if date_info["year"] == -1:
+ year_match = re.search(patterns["year"], input_string)
+ if year_match:
+ date_info["year"] = int(year_match.group(0))
+
+ # Extract month if not already found
+ if date_info["month"] == -1:
+ month_match = re.search(patterns["month"], input_string)
+ if month_match:
+ date_info["month"] = month_mapping[month_match.group(0)]
+
+ # Extract 12-hour format time
+ hour_12_match = re.search(patterns["hour_12"], input_string)
+ if hour_12_match:
+ hour, period = int(hour_12_match.group(1)), hour_12_match.group(2).lower()
+ if period == 'pm' and hour != 12:
+ hour += 12
+ elif period == 'am' and hour == 12:
+ hour = 0
+ date_info["hour"] = hour
+
+ # Identify weekday
+ for week_day, value in weekday_mapping.items():
+ if week_day in input_string:
+ date_info["weekday"] = value
+ break
+
+ return date_info
+
+ @classmethod
+ def extract_date_parts(cls, input_string: str, language: LanguageEnum) -> dict:
+ """
+ Extracts various date components from the input string based on the current language context.
+
+ This method dynamically selects a language-specific function to parse the input string and extract
+ date parts such as year, month, day, etc. If the function for current language context does not exist,
+ a warning is logged and an empty dictionary is returned.
+
+ Args:
+ input_string (str): The string containing date information to be parsed.
+ language (str): current language.
+
+ Returns:
+ dict: A dictionary containing extracted date components, or an empty dictionary if parsing fails.
+ """
+ func_name = f"extract_date_parts_{language.value}"
+ if not hasattr(cls, func_name):
+ # cls.logger.warning(f"language={language.value} needs to complete extract_date_parts func!")
+ return {}
+ return getattr(cls, func_name)(input_string=input_string)
+
+ @classmethod
+ def has_time_word_cn(cls, query: str, datetime_word_list: List[str]) -> bool:
+ """
+ Check if the input query contains any datetime-related words based on the cn language context.
+
+ Args:
+ query (str): The input string to check for datetime-related words.
+ datetime_word_list (list[str]): datetime keywords
+
+ Returns:
+ bool: True if the query contains at least one datetime-related word, False otherwise.
+ """
+ contain_datetime = False
+ # TODO use re
+ for datetime_word in datetime_word_list:
+ if datetime_word in query:
+ contain_datetime = True
+ break
+ return contain_datetime
+
+ @classmethod
+ def has_time_word_en(cls, query: str, datetime_word_list: List[str]) -> bool:
+ """
+ Check if the input query contains any datetime-related words based on the en language context.
+
+ Args:
+ query (str): The input string to check for datetime-related words.
+ datetime_word_list (list[str]): datetime keywords
+
+ Returns:
+ bool: True if the query contains at least one datetime-related word, False otherwise.
+ """
+ contain_datetime = False
+ for datetime_word in datetime_word_list:
+ datetime_word = datetime_word.lower()
+ # TODO fix strip
+ if datetime_word in [x.strip().lower().strip(",").strip(".").strip("?").strip(":")
+ for x in query.split(" ")]:
+ contain_datetime = True
+ break
+ return contain_datetime
+
+ @classmethod
+ def has_time_word(cls, query: str, language: LanguageEnum) -> bool:
+ func_name = f"has_time_word_{language.value}"
+ if not hasattr(cls, func_name):
+ # cls.logger.warning(f"language={language.value} needs to complete has_time_word function!")
+ return False
+
+ if language not in DATATIME_WORD_LIST:
+ # cls.logger.warning(f"language={language.value} is missing in DATATIME_WORD_LIST!")
+ return False
+
+ datetime_word_list = DATATIME_WORD_LIST[language]
+ return getattr(cls, func_name)(query=query, datetime_word_list=datetime_word_list)
+
+ def datetime_format(self, dt_format: str = "%Y%m%d") -> str:
+ """
+ Format the stored datetime object into a string based on the provided format.
+
+ Args:
+ dt_format (str, optional): The datetime format string. Defaults to "%Y%m%d".
+
+ Returns:
+ str: A formatted datetime string.
+ """
+ return self._dt.strftime(dt_format)
+
+ def string_format(self, string_format: str, language: LanguageEnum) -> str:
+ """
+ Format the datetime information stored in the instance using a custom string format.
+
+ Args:
+ string_format (str): A format string where placeholders are keys from `dt_info_dict`.
+ language (str): current language.
+
+ Returns:
+ str: A formatted datetime string.
+ """
+ return string_format.format(**self.get_dt_info_dict(language=language))
+
+ @property
+ def timestamp(self) -> int:
+ """
+ Get the timestamp representation of the stored datetime.
+
+ Returns:
+ int: A timestamp value.
+ """
+ return int(self._dt.timestamp())
diff --git a/memoryscope/memoryscope/core/utils/logger.py b/memoryscope/memoryscope/core/utils/logger.py
new file mode 100644
index 00000000..6755113a
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/logger.py
@@ -0,0 +1,248 @@
+import os
+import logging
+import pprint
+from logging.handlers import RotatingFileHandler
+from pathlib import Path
+from rich.console import Console
+from rich.panel import Panel
+from rich.text import Text
+
+LOG_FORMAT = "%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s"
+DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+LOGGER_DICT = {}
+
+def rich2text(rich_table):
+ console = Console(width=150)
+ with console.capture() as capture:
+ console.print(rich_table)
+ return '\n' + str(Text.from_ansi(capture.get()))
+
+def append_memoryscope_uuid(dir_path):
+ from memoryscope.core.memoryscope_context import get_memoryscope_uuid # pylint: disable=import-outside-toplevel
+ dir_path = os.path.join(dir_path, get_memoryscope_uuid())
+ return dir_path
+
+class Logger(logging.Logger):
+ """
+ The `Logger` class handle the stream of information or errors in activities.
+ """
+
+ def __init__(self,
+ name: str,
+ level: int = logging.INFO,
+ format_style: str = LOG_FORMAT,
+ date_format_style: str = DATE_FORMAT,
+ to_stream: bool = False,
+ to_file: bool = True,
+ file_mode: str = "w",
+ file_type: str = "log",
+ dir_path: str = "log",
+ max_bytes: int = 1024 * 1024 * 1024,
+ backup_count: int = 10):
+ """
+ Initializes the Logger instance, setting up handlers for console and file logging based on provided parameters.
+
+ Args:
+ name (str): Identifier for the logger.
+ level (int, optional): Logging level. Defaults to logging.INFO.
+ format_style (str, optional): Log message format. Defaults to LOG_FORMAT constant.
+ date_format_style (str, optional): Date format for logs. Defaults to DATE_FORMAT constant.
+ to_stream (bool, optional): Enables console logging. Defaults to True.
+ to_file (bool, optional): Enables file logging. Defaults to True.
+ file_mode (str, optional): File open mode. Defaults to 'w'.
+ file_type (str, optional): Log file extension type. Defaults to 'log'.
+ dir_path (str, optional): Directory for log files. Defaults to 'log'.
+ max_bytes (int, optional): Maximum log file size before rotation. Defaults to 1GB.
+ backup_count (int, optional): Number of rotated log files to retain. Defaults to 10.
+ """
+ super(Logger, self).__init__(name, level)
+
+ self.formatter = logging.Formatter(format_style, date_format_style)
+ self.date_format_style = date_format_style
+ self.to_stream: bool = to_stream
+ self.to_file: bool = to_file
+ self.file_mode: str = file_mode
+ self.file_type: str = file_type
+ self.dir_path: str = dir_path
+
+ self.max_bytes: int = max_bytes
+ self.backup_count: int = backup_count
+
+ self.trace_id: str = ""
+
+ if self.to_stream:
+ self._add_stream_handler() # Adds a handler to output logs to the console
+ if self.to_file:
+ self._add_file_handler() # Adds a handler to output logs to a file
+
+ self.info(f"logger={name} is inited.") # Logs an initialization message
+
+ def log_dictionary_info(self, dictionary, title=""):
+ self.info(self.format_current_context(dictionary, title))
+
+ def format_current_context(self, context, title=""):
+ pp = pprint.PrettyPrinter()
+ pretty_string = pp.pformat(context)
+ if title:
+ pretty_string = f"{title}\n{pretty_string}"
+ return self.wrap_in_box(pretty_string)
+
+ def wrap_in_box(self, context):
+ return rich2text(Panel(context, width=128))
+
+ def format_chat_message(self, message):
+ buf = []
+ buf.append('\n')
+ buf.append(f"LM Input:\n")
+ for chat_message in message.meta_data['data']['messages']:
+ buf.append(chat_message.content)
+ buf.append('\n')
+ buf.append(f"------------------------------------------\n")
+ buf.append(f"LM Output:\n")
+ buf.append(message.message.content)
+ buf.append('\n')
+ buf.append('\n')
+ return self.wrap_in_box(''.join(buf))
+
+ def format_rank_message(self, model_response):
+ buf = []
+ buf.append('\n')
+ buf.append(f"Query Input:\n")
+ buf.append(model_response.meta_data['data']['query_str'])
+ buf.append('\n')
+ buf.append(f"------------------------------------------\n")
+ buf.append(f"Rank:\n")
+ rank = 0
+ for index, score in model_response.rank_scores.items():
+ rank += 1
+ node = model_response.meta_data['data']['nodes'][index]
+ node_text = node.text
+ buf.append(f"Score {score} | Rank {rank} | {node_text}\n")
+ buf.append('\n')
+ buf.append('\n')
+ return self.wrap_in_box(''.join(buf))
+
+ def _add_file_handler(self):
+ """
+ Adds a file handler to the logger which logs messages to a rotating file.
+
+ The file is stored in a specified directory with a name derived from the logger's name and type.
+ The file handler is set up to rotate when it reaches a certain size and keeps a defined number of backups.
+
+ This method ensures the directory exists before creating the file handler and sets the formatter
+ for consistent log message formatting.
+ """
+ file_path = Path().joinpath(self.dir_path, f"{self.name}.{self.file_type}")
+ os.makedirs(file_path.parent, exist_ok=True) # Ensure the directory exists
+ file_name = file_path.as_posix() # Get the absolute path as a string
+ if not hasattr(Logger, 'notice_print'):
+ Console().print(f"\nRegistering loggers at: {os.path.abspath(os.path.dirname(file_name))}. System logs can be found in this directory.\n", style="bold red")
+ Logger.notice_print = True
+ # Instantiate a rotating file handler with specified parameters
+ file_handler = RotatingFileHandler(
+ filename=file_name,
+ maxBytes=self.max_bytes, # Maximum size of the log file before rotation
+ backupCount=self.backup_count, # Number of backup files to keep
+ encoding="utf-8") # Set the encoding to UTF-8
+ file_handler.setFormatter(self.formatter) # Apply the logger's formatter to the handler
+ self.addHandler(file_handler) # Add the file handler to this logger instance
+
+ def _add_stream_handler(self):
+ """
+ Adds a stream handler to the logger for console output. The handler is configured
+ with the logger's formatter and set to use UTF-8 encoding.
+ """
+ stream_handler = logging.StreamHandler()
+ stream_handler.setFormatter(self.formatter) # Configure the handler with the logger's formatter
+ stream_handler.encoding = 'utf-8' # Set the handler's encoding to UTF-8
+ self.addHandler(stream_handler) # Add the handler to the logger
+
+ def close(self):
+ """
+ Closes all handlers associated with this logger instance.
+
+ This method iterates over the handlers attached to the logger and
+ calls their `close` method to ensure that any system resources used
+ by the handlers are freed properly.
+ """
+ for handler in self.handlers:
+ # Close each handler to release resources
+ handler.close()
+
+ def clear(self):
+ """
+ Clears all handlers from the logger.
+ """
+ self.handlers.clear()
+
+ def set_trace_id(self, trace_id: str):
+ """
+ Sets the trace ID for the logger. If the provided trace ID is longer than 8 characters,
+ it will be truncated to the first 8 characters.
+
+ Args:
+ trace_id (str): The trace identifier to be associated with the logs.
+ """
+ self.trace_id: str = trace_id
+ if len(self.trace_id) >= 8:
+ self.trace_id = self.trace_id[:8]
+
+ def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
+ func=None, extra=None, sinfo=None):
+ """
+ Creates a log record with additional trace_id included in the extra information.
+
+ This method extends the default behavior of creating a log record by adding
+ a trace_id from the logger instance to the record's extra data, allowing
+ for traceability within logged data.
+
+ Args:
+ name (str): The name of the logger.
+ level (int): The logging level of the record.
+ fn (str): The name of the function containing the logging call.
+ lno (int): The line number at which the logging call was made.
+ msg (str): The logged message, before formatting.
+ args (tuple): The arguments to the log message.
+ exc_info (tuple): Exception information or None.
+ func (function): The function where the logging call was made. Defaults to None.
+ extra (dict): Additional information for the log record. Defaults to None.
+ sinfo (str): Stack trace information or None.
+
+ Returns:
+ logging.LogRecord: The created log record with potentially enriched 'extra' field.
+ """
+ if extra is None:
+ extra = {}
+ if self.trace_id:
+ extra["trace_id"] = self.trace_id # Include trace_id from the logger in the log record extra data
+ return super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
+
+ @classmethod
+ def get_logger(cls, name: str = None, **kwargs):
+ """
+ Retrieves or creates a logger instance with the specified name and configurations.
+
+ If no name is provided, it defaults to the first registered logger's name or 'default' if none exist.
+ This method ensures that only one logger instance exists per name by reusing existing instances
+ stored in `LOGGER_DICT`.
+
+ Args:
+ name (str, optional): The name of the logger. Defaults to None, which triggers auto-naming logic.
+ **kwargs: Additional keyword arguments to configure the logger.
+
+ Returns:
+ Logger: The requested or newly created logger instance.
+ """
+ if name is None:
+ if LOGGER_DICT:
+ name = list(LOGGER_DICT.keys())[0]
+ else:
+ name = "default"
+
+ if name not in LOGGER_DICT:
+ logger_dir = kwargs.get('dir_path', 'log')
+ logger_dir = append_memoryscope_uuid(logger_dir)
+ LOGGER_DICT[name] = Logger(name=name, dir_path=logger_dir, **kwargs)
+
+ return LOGGER_DICT[name]
diff --git a/memoryscope/memoryscope/core/utils/prompt_handler.py b/memoryscope/memoryscope/core/utils/prompt_handler.py
new file mode 100644
index 00000000..9001b7e9
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/prompt_handler.py
@@ -0,0 +1,164 @@
+import json
+import os.path
+from pathlib import Path
+from typing import Dict
+
+import yaml
+
+from memoryscope.enumeration.language_enum import LanguageEnum
+
+
+class PromptHandler(object):
+ """
+ The `PromptHandler` class manages prompt messages by loading them from YAML or JSON files and dictionaries,
+ supporting language selection based on a context, and providing dictionary-like access to the prompt messages.
+ """
+
+ def __init__(self,
+ class_path: str,
+ language: LanguageEnum | str,
+ prompt_file: str = "",
+ prompt_dict: dict = None,
+ **kwargs):
+ """
+ Initializes the PromptHandler with paths to prompt sources and additional keyword arguments.
+
+ Args:
+ class_path (str): The path to the class where prompts are utilized.
+ prompt_file (str, optional): The path to an external file containing prompts. Defaults to "".
+ prompt_dict (dict, optional): A dictionary directly containing prompt definitions. Defaults to None.
+ language (LanguageEnum, str): context language.
+ **kwargs: Additional keyword arguments that might be used in prompt handling.
+ """
+ class_path: Path = Path(class_path)
+ self._class_dir: Path = class_path.parent
+ self._class_name: str = class_path.stem
+ self._language_enum: LanguageEnum = LanguageEnum(language)
+ self.kwargs = kwargs
+
+ self._prompt_dict: Dict[str, str] = {}
+
+ self.add_prompt_file((self._class_dir / self._class_name).__str__(), raise_exception=False)
+ if prompt_file:
+ self.add_prompt_file((self._class_dir / prompt_file).__str__())
+ if prompt_dict:
+ self.add_prompt_dict(prompt_dict)
+
+ @staticmethod
+ def file_path_completion(file_path: str, raise_exception: bool = True) -> str:
+ """
+ Attempts to complete the given file path by appending either a `.yaml` or `.json` extension
+ based on the existence of the respective file. If neither exists, an exception is raised.
+
+ Args:
+ file_path (str): The base path of the file to be completed.
+ raise_exception (bool): If the file cannot be found, report an error.
+
+ Returns:
+ str: The completed file path with the appropriate extension.
+
+ Raises:
+ RuntimeError: If neither the `.yaml` nor `.json` file exists at the given path.
+ """
+ if file_path.endswith(".yaml") or file_path.endswith(".json"):
+ return file_path
+
+ if os.path.exists(f"{file_path}.yaml"):
+ return f"{file_path}.yaml"
+
+ if os.path.exists(f"{file_path}.json"):
+ return f"{file_path}.json"
+
+ if raise_exception:
+ raise RuntimeError(f"{file_path}/yaml/json is not exists!")
+
+ def add_prompt_file(self, file_path: str, raise_exception: bool = True):
+ """
+ Adds prompt messages from a YAML or JSON file to the internal dictionary.
+
+ This method supports loading prompts from files ending with '.yaml' or '.json'.
+ It uses the respective libraries to parse the content and merge it into the current prompt dictionary.
+
+ Args:
+ file_path (str): The path to the YAML or JSON file containing the prompts.
+ raise_exception (bool): If the file cannot be found, report an error.
+ """
+ file_path = self.file_path_completion(file_path, raise_exception=raise_exception)
+ if not file_path:
+ return
+
+ prompt_dict = {}
+
+ if file_path.endswith(".yaml"):
+ # Load prompts from a YAML file
+ with open(file_path) as f:
+ prompt_dict = yaml.load(f, yaml.FullLoader)
+
+ elif file_path.endswith(".json"):
+ # Load prompts from a JSON file (corrected file handling)
+ with open(file_path) as f:
+ prompt_dict = json.load(f)
+
+ # Merge the loaded prompts into the existing dictionary
+ self.add_prompt_dict(prompt_dict)
+
+ def add_prompt_dict(self, prompt_dict: dict):
+ """
+ Adds prompt messages from a dictionary, ensuring each message has a valid entry for the current language.
+
+ Args:
+ prompt_dict (dict): A dictionary where keys represent prompt identifiers and values are nested dictionaries
+ containing language-specific prompt messages.
+
+ Raises:
+ RuntimeError: If a prompt message for the current language is not found.
+ """
+ for key, language_dict in prompt_dict.items():
+ prompts = language_dict.get(self._language_enum.value)
+ if not prompts:
+ raise RuntimeError(f"{key}.prompt.{self._language_enum.value} is empty!")
+ self._prompt_dict[key] = prompts.strip()
+
+ @property
+ def prompt_dict(self) -> dict:
+ """
+ Retrieves the internal dictionary containing all prompt messages.
+
+ Returns:
+ dict: The dictionary of prompt messages with keys as identifiers and values as prompt strings.
+ """
+ return self._prompt_dict
+
+ def __getitem__(self, key: str) -> str:
+ """
+ Enables accessing prompt messages using dictionary-like indexing.
+
+ Args:
+ key (str): The identifier for the prompt message.
+
+ Returns:
+ str: The prompt message corresponding to the given key.
+ """
+ return self._prompt_dict[key]
+
+ def __setitem__(self, key: str, value: str):
+ """
+ Allows setting prompt messages using dictionary-like item assignment.
+
+ Args:
+ key (str): The identifier for the prompt message.
+ value (str): The new prompt message content.
+ """
+ self._prompt_dict[key] = value
+
+ def __getattr__(self, key: str) -> str:
+ """
+ Overrides attribute access to provide prompt messages dynamically.
+
+ Args:
+ key (str): The identifier for the prompt message attempted to access as an attribute.
+
+ Returns:
+ str: The prompt message corresponding to the given attribute-like key.
+ """
+ return self._prompt_dict[key]
diff --git a/memoryscope/memoryscope/core/utils/registry.py b/memoryscope/memoryscope/core/utils/registry.py
new file mode 100644
index 00000000..9c942851
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/registry.py
@@ -0,0 +1,81 @@
+"""
+Registry for different modules.
+Init class according to the class name and verify the input parameters.
+"""
+from typing import Dict, Any, List
+
+
+class Registry(object):
+ """
+ A registry to manage and instantiate various modules by their names, ensuring the uniqueness of registered entries.
+ It supports both individual and bulk registration of modules, as well as retrieval of modules by name.
+
+ Attributes:
+ name (str): The name of the registry.
+ module_dict (Dict[str, Any]): A dictionary holding registered modules where keys are module names and values are
+ the modules themselves.
+ """
+
+ def __init__(self, name: str):
+ """
+ Initializes the Registry with a given name.
+
+ Args:
+ name (str): The name to identify this registry.
+ """
+ self.name: str = name
+ self.module_dict: Dict[str, Any] = {}
+
+ def register(self, module_name: str = None, module: Any = None):
+ """
+ Registers module in the registry in a single call.
+
+ Args:
+ module_name (str): The name of module to be registered.
+ module (List[Any] | Dict[str, Any]): The module to be registered.
+
+ Raises:
+ NotImplementedError: If the input is already registered.
+ """
+ assert module is not None
+ if module_name is None:
+ module_name = module.__name__
+
+ if module_name in self.module_dict:
+ raise KeyError(f'{module_name} is already registered in {self.name}')
+ self.module_dict[module_name] = module
+
+ def batch_register(self, modules: List[Any] | Dict[str, Any]):
+ """
+ Registers multiple modules in the registry in a single call. Accepts either a list of modules or a dictionary
+ mapping names to modules.
+
+ Args:
+ modules (List[Any] | Dict[str, Any]): A list of modules or a dictionary mapping module names to the modules.
+
+ Raises:
+ NotImplementedError: If the input is neither a list nor a dictionary.
+ """
+ if isinstance(modules, list):
+ module_name_dict = {m.__name__: m for m in modules}
+ elif isinstance(modules, dict):
+ module_name_dict = modules
+ else:
+ raise NotImplementedError("Input must be a list or a dictionary.")
+ self.module_dict.update(module_name_dict)
+
+ def __getitem__(self, module_name: str):
+ """
+ Retrieves a registered module by its name using index notation.
+
+ Args:
+ module_name (str): The name of the module to retrieve.
+
+ Returns:
+ A registered module corresponding to the given name.
+
+ Raises:
+ AssertionError: If the specified module is not found in the registry.
+ """
+ assert module_name in self.module_dict, f"{module_name} not found in {self.name}"
+ return self.module_dict[module_name]
diff --git a/memoryscope/memoryscope/core/utils/response_text_parser.py b/memoryscope/memoryscope/core/utils/response_text_parser.py
new file mode 100644
index 00000000..d74b3141
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/response_text_parser.py
@@ -0,0 +1,60 @@
+import re
+from typing import List
+
+from memoryscope.constants.language_constants import NONE_WORD
+from memoryscope.core.utils.logger import Logger
+from memoryscope.enumeration.language_enum import LanguageEnum
+
+
+class ResponseTextParser(object):
+ """
+ The `ResponseTextParser` class is designed to parse and process response texts. It provides methods to extract
+ patterns from the text and filter out unnecessary information, while also logging the processing steps and outcomes.
+ """
+
+ PATTERN_V1 = re.compile(r"<(.*?)>") # Regular expression pattern to match content within angle brackets
+
+ def __init__(self, response_text: str, language: LanguageEnum, logger_prefix: str = ""):
+ # Strips leading and trailing whitespace from the response text
+ self.response_text: str = response_text.strip()
+ self.language: LanguageEnum = language
+
+ # The prefix of log. Defaults to "".
+ self.logger_prefix: str = logger_prefix
+
+ # Initializes a logger instance for logging parsing activities
+ self.logger: Logger = Logger.get_logger()
+
+ def parse_v1(self) -> List[List[str]]:
+ """
+ Extract specific patterns from the text which match content within angle brackets.
+
+ Returns:
+ Contents match the specific patterns.
+ """
+ result = []
+ for line in self.response_text.split("\n"):
+ line = line.strip()
+ if not line:
+ continue
+ matches = [match.group(1) for match in self.PATTERN_V1.finditer(line)]
+ if matches:
+ result.append(matches)
+ self.logger.info(f"{self.logger_prefix} response_text={self.response_text} result={result}", stacklevel=2)
+ return result
+
+ def parse_v2(self) -> List[str]:
+ """
+ Extract lines which contain NONE_WORD.
+
+ Returns:
+ Contents match the specific patterns.
+ """
+ result = []
+ for line in self.response_text.split("\n"):
+ line = line.strip()
+ if not line or line.lower() == NONE_WORD.get(self.language):
+ continue
+ result.append(line)
+ self.logger.info(f"{self.logger_prefix} response_text={self.response_text} result={result}", stacklevel=2)
+ return result
diff --git a/memoryscope/memoryscope/core/utils/singleton.py b/memoryscope/memoryscope/core/utils/singleton.py
new file mode 100644
index 00000000..b767cd1e
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/singleton.py
@@ -0,0 +1,9 @@
+def singleton(cls):
+ _instance = {}
+
+ def _singleton(*args, **kargs):
+ if cls not in _instance:
+ _instance[cls] = cls(*args, **kargs)
+ return _instance[cls]
+
+ return _singleton
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/utils/timer.py b/memoryscope/memoryscope/core/utils/timer.py
new file mode 100644
index 00000000..4ba40903
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/timer.py
@@ -0,0 +1,130 @@
+import time
+from typing import Literal
+
+from memoryscope.core.utils.logger import Logger
+
+TIME_LOG_TYPE = Literal["end", "wrap", "none"]
+
+
+class Timer(object):
+ """
+ A class used to measure the execution time of code blocks. It supports logging the elapsed time and can be
+ customized to display time in seconds or milliseconds.
+ """
+
+ def __init__(self,
+ name: str,
+ time_log_type: TIME_LOG_TYPE = "end",
+ use_ms: bool = True,
+ stack_level: int = 2,
+ float_precision: int = 4,
+ **kwargs):
+
+ """
+ Initializes the `Timer` instance with the provided args and sets up a logger
+
+ Args:
+ name (str): The log name.
+ time_log_type (str): The log type. Defaults to 'End'.
+ use_ms (bool): Use 'ms' as the timescale or not. Defaults to True.
+ stack_level (int): The stack level of log. Defaults to 2.
+ float_precision (int): The precision of cost time. Defaults to 4.
+
+ """
+
+ self.name: str = name
+ self.time_log_type: TIME_LOG_TYPE = time_log_type
+ self.use_ms: bool = use_ms
+ self.stack_level: int = stack_level
+ self.float_precision: int = float_precision
+ self.kwargs: dict = kwargs
+
+ # time recorder
+ self.t_start = 0
+ self.t_end = 0
+ self.cost = 0
+
+ self.logger = Logger.get_logger()
+
+ def _set_cost(self):
+ """
+ Accumulate the cost time.
+ """
+ self.t_end = time.time()
+ self.cost = self.t_end - self.t_start
+ if self.use_ms:
+ self.cost *= 1000
+
+ @property
+ def cost_str(self):
+ """
+ Represent the cost time into a formatted string.
+ """
+ self._set_cost()
+ if self.use_ms:
+ return f"cost={self.cost:.4f}ms"
+ else:
+ return f"cost={self.cost:.4f}s"
+
+ def __enter__(self, *args, **kwargs):
+ """
+ Begin timing.
+ """
+ self.t_start = time.time()
+ if self.time_log_type == "wrap":
+ self.logger.info(f"----- {self.name}.begin -----")
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ """
+ End timing and print the formatted log.
+ """
+ if self.time_log_type == "none":
+ return
+
+ lines = []
+ if self.time_log_type == "wrap":
+ lines.append(f"----- {self.name}.end -----")
+ else:
+ lines.append(self.name)
+
+ lines.append(self.cost_str)
+
+ if self.kwargs:
+ for k, v in self.kwargs.items():
+ if isinstance(v, float):
+ float_style = f".{self.float_precision}f"
+ line = f"{k}={v:{float_style}}"
+ else:
+ line = f"{k}={v}"
+ lines.append(line)
+
+ self.logger.info(" ".join(lines), stacklevel=self.stack_level)
+
+
+def timer(func):
+ """
+ A decorator function that measures the execution time of the wrapped function.
+
+ Args:
+ func (Callable): The function to be wrapped and timed.
+
+ Returns:
+ Callable: The wrapper function that includes timing functionality.
+ """
+
+ def wrapper(*args, **kwargs):
+ """
+ The wrapper function that manages the timing of the original function.
+
+ Args:
+ *args: Variable length argument list for the decorated function.
+ **kwargs: Arbitrary keyword arguments for the decorated function.
+
+ Returns:
+ Any: The result of the decorated function.
+ """
+ with Timer(name=func.__name__, **kwargs):
+ return func(*args, **kwargs)
+
+ return wrapper
diff --git a/memoryscope/memoryscope/core/utils/tool_functions.py b/memoryscope/memoryscope/core/utils/tool_functions.py
new file mode 100644
index 00000000..9e4694ba
--- /dev/null
+++ b/memoryscope/memoryscope/core/utils/tool_functions.py
@@ -0,0 +1,220 @@
+import hashlib
+import random
+import re
+import time
+from copy import deepcopy
+from importlib import import_module
+from typing import List
+
+import numpy as np
+import pyfiglet
+from termcolor import colored
+
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.scheme.message import Message
+
+ALL_COLORS = ["red", "green", "yellow", "blue", "magenta", "cyan", "light_grey", "light_red", "light_green",
+ "light_yellow", "light_blue", "light_magenta", "light_cyan", "white"]
+
+
+def underscore_to_camelcase(name: str, is_first_title: bool = True) -> str:
+ """
+ Converts an underscore_notation string to CamelCase.
+
+ Args:
+ name (str): The underscore_notation string to be converted.
+ is_first_title (bool): Title the first word or not. Defaults to True
+
+ Returns:
+ str: A CamelCase formatted string.
+ """
+ name_split = name.split("_")
+ if is_first_title:
+ return "".join(x.title() for x in name_split)
+ else:
+ return name_split[0] + ''.join(x.title() for x in name_split[1:])
+
+
+def camelcase_to_underscore(name: str) -> str:
+ """
+ Converts a CamelCase string to underscore_notation.
+
+ Args:
+ name (str): The CamelCase formatted string to be converted.
+
+ Returns:
+ str: A converted string in underscore_notation.
+ """
+ return re.sub(r'(? List[Message]:
+ """
+ Converts input strings into a structured list of message objects suitable for AI interactions.
+
+ Args:
+ system_prompt (str): The system-level instruction or context.
+ few_shot (str): An example or demonstration input, often used for illustrating expected behavior.
+ user_query (str): The actual user query or prompt to be processed.
+ concat_system_prompt(bool): Concat system prompt again or not in the user message.
+ A simple method to improve the effectiveness for some LLMs. Defaults to True.
+
+ Returns:
+ List[Message]: A list of Message objects, each representing a part of the conversation setup.
+ """
+
+ system_message = Message(role=MessageRoleEnum.SYSTEM.value, content=system_prompt.strip())
+ if concat_system_prompt:
+ user_content_list = [system_prompt, few_shot, user_query]
+ else:
+ user_content_list = [few_shot, user_query]
+ user_message = Message(role=MessageRoleEnum.USER.value, content="\n".join([x.strip() for x in user_content_list]))
+ return [system_message, user_message]
+
+
+def char_logo(words: str, seed: int = time.time_ns(), color=None):
+ """
+ Render the context of logo with colors
+
+ Args:
+ words: The context of logo.
+ seed: The random seed which generates colors if there is no specific color. Defaults to the current timestamp.
+ color: The specific color. Defaults to None.
+
+ Returns:
+ A rendered logo
+ """
+ font = pyfiglet.Figlet()
+ rendered_text = font.renderText(words)
+ colored_lines = []
+ all_colors = ALL_COLORS.copy()
+ random.seed = seed
+ for line in rendered_text.splitlines():
+ line_color = color
+ if line_color is None:
+ random.shuffle(all_colors)
+ line_color = all_colors[0]
+ colored_line = ""
+ for char in line:
+ colored_char = colored(char, line_color, attrs=['bold'])
+ colored_line += colored_char
+ colored_lines.append(colored_line)
+ return colored_lines
+
+
+def md5_hash(input_string: str) -> str:
+ """
+ Computes a MD5 hash of the given input string.
+
+ Args:
+ input_string (str): The string for which the MD5 hash needs to be computed.
+
+ Returns:
+ str: A hexadecimal MD5 hash representation.
+ """
+ m = hashlib.md5()
+ m.update(input_string.encode('utf-8'))
+ return m.hexdigest()
+
+
+def contains_keyword(text, keywords) -> bool:
+ """
+ Checks if the given text contains any of the specified keywords, ignoring case.
+
+ Args:
+ text (str): The text to search within.
+ keywords (List[str]): A list of keywords to look for in the text.
+
+ Returns:
+ bool: True if any keyword is found in the text, False otherwise.
+ """
+ escaped_keywords = map(re.escape, keywords)
+ pattern = re.compile('|'.join(escaped_keywords), re.IGNORECASE)
+ return pattern.search(text) is not None
+
+
+def cosine_similarity(query: List[float], documents: List[List[float]]):
+ query = np.array(query)
+ documents = np.array(documents)
+
+ query_norm = np.linalg.norm(query)
+ if query_norm == 0:
+ raise ValueError("Query vector norm is zero, which will result in a division by zero")
+
+ documents_norm = np.linalg.norm(documents, axis=1)
+ if np.any(documents_norm == 0):
+ raise ValueError("One of the document vectors has zero norm, which will result in a division by zero")
+
+ dot_product = np.dot(documents, query)
+
+ cosine_similarities = dot_product / (query_norm * documents_norm)
+ return cosine_similarities.tolist()
+
+
+def cosine_similarity_matrix(query: List[List[float]]):
+ query = np.array(query)
+
+ documents_norm = np.linalg.norm(query, axis=1)
+ if np.any(documents_norm == 0):
+ raise ValueError("One of the document vectors has zero norm, which will result in a division by zero")
+
+ n_query = query.shape[0]
+ query_expanded = np.expand_dims(query, axis=0)
+ query_triplicated = np.repeat(query_expanded, repeats=n_query, axis=0)
+ query_transpose = query_triplicated.swapaxes(0, 1)
+
+ q = np.expand_dims(documents_norm, axis=0)
+ norm_dot = q.transpose() * q
+ dot_product = (query_triplicated*query_transpose).sum(-1) / norm_dot
+
+ return dot_product
+
+
diff --git a/memoryscope/memoryscope/core/worker/__init__.py b/memoryscope/memoryscope/core/worker/__init__.py
new file mode 100644
index 00000000..49830f30
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/__init__.py
@@ -0,0 +1,11 @@
+from .base_worker import BaseWorker
+from .dummy_worker import DummyWorker
+from .memory_base_worker import MemoryBaseWorker
+from .memory_manager import MemoryManager
+
+__all__ = [
+ "BaseWorker",
+ "DummyWorker",
+ "MemoryBaseWorker",
+ "MemoryManager"
+]
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/worker/backend/__init__.py b/memoryscope/memoryscope/core/worker/backend/__init__.py
new file mode 100644
index 00000000..01d3682e
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/__init__.py
@@ -0,0 +1,21 @@
+from .contra_repeat_worker import ContraRepeatWorker
+from .get_observation_with_time_worker import GetObservationWithTimeWorker
+from .get_observation_worker import GetObservationWorker
+from .get_reflection_subject_worker import GetReflectionSubjectWorker
+from .info_filter_worker import InfoFilterWorker
+from .load_memory_worker import LoadMemoryWorker
+from .long_contra_repeat_worker import LongContraRepeatWorker
+from .update_insight_worker import UpdateInsightWorker
+from .update_memory_worker import UpdateMemoryWorker
+
+__all__ = [
+ "ContraRepeatWorker",
+ "GetObservationWithTimeWorker",
+ "GetObservationWorker",
+ "GetReflectionSubjectWorker",
+ "InfoFilterWorker",
+ "LoadMemoryWorker",
+ "LongContraRepeatWorker",
+ "UpdateInsightWorker",
+ "UpdateMemoryWorker"
+]
diff --git a/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.py b/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.py
new file mode 100644
index 00000000..9507b23b
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.py
@@ -0,0 +1,129 @@
+from typing import List
+
+from memoryscope.constants.common_constants import NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES, MERGE_OBS_NODES, TODAY_NODES
+from memoryscope.constants.language_constants import NONE_WORD, CONTRADICTORY_WORD, CONTAINED_WORD
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class ContraRepeatWorker(MemoryBaseWorker):
+ """
+ The `ContraRepeatWorker` class specializes in processing memory nodes to identify and handle
+ contradictory and repetitive information. It extends the base functionality of `MemoryBaseWorker`.
+
+ Responsibilities:
+ - Collects observation nodes from various memory categories.
+ - Constructs a prompt with these observations for language model analysis.
+ - Parses the model's response to detect contradictions or redundancies.
+ - Adjusts the status of memory nodes based on the analysis.
+ - Persists the updated node statuses back into memory.
+ """
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+ self.retrieve_top_k: int = kwargs.get("retrieve_top_k", 30)
+ self.contra_repeat_max_count: int = kwargs.get("contra_repeat_max_count", 50)
+ self.enable_today_contra_repeat: bool = self.memoryscope_context.meta_data["enable_today_contra_repeat"]
+
+ def _run(self):
+ """
+ Executes the primary routine of the ContraRepeatWorker which involves fetching memory nodes,
+ constructing a prompt, querying a language model, parsing the response to identify nodes for merging,
+ updating node statuses, and saving the updated nodes back to memory.
+
+ Steps:
+ 1. Retrieves new observation nodes and nodes observed on the current day.
+ 2. Optionally combines today's nodes with the new ones, sorts, and limits the list by a predefined count.
+ 3. Constructs a prompt using the combined nodes, system prompt, and a few-shot example.
+ 4. Queries a language model with the constructed prompt.
+ 5. Parses the model's response to identify nodes to merge or exclude based on contradiction or redundancy.
+ 6. Updates the status of nodes accordingly.
+ 7. Persists the changes back to memory storage.
+ """
+ if not self.enable_today_contra_repeat:
+ self.logger.warning("today_contra_repeat is not enabled!")
+ return
+
+ all_obs_nodes: List[MemoryNode] = self.memory_manager.get_memories([NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES])
+ if not all_obs_nodes:
+ self.logger.info("all_obs_nodes is empty!")
+ # self.continue_run = False
+ return
+
+ today_obs_nodes: List[MemoryNode] = self.memory_manager.get_memories(TODAY_NODES)
+
+ if today_obs_nodes:
+ all_obs_nodes.extend(today_obs_nodes)
+ all_obs_nodes = sorted(all_obs_nodes, key=lambda x: x.timestamp, reverse=True)[:self.contra_repeat_max_count]
+
+ if len(all_obs_nodes) == 1:
+ self.logger.info("all_obs_nodes.size=1, stop.")
+ return
+
+ # build prompt
+ user_query_list = []
+ for i, n in enumerate(all_obs_nodes):
+ user_query_list.append(f"{i + 1} {n.content}")
+
+ system_prompt = self.prompt_handler.contra_repeat_system.format(num_obs=len(user_query_list),
+ user_name=self.target_name)
+ few_shot = self.prompt_handler.contra_repeat_few_shot.format(user_name=self.target_name)
+ user_query = self.prompt_handler.contra_repeat_user_query.format(user_query="\n".join(user_query_list))
+ contra_repeat_message = self.prompt_to_msg(system_prompt=system_prompt, few_shot=few_shot,
+ user_query=user_query)
+ self.logger.info(f"contra_repeat_message={contra_repeat_message}")
+
+ # call LLM
+ response = self.generation_model.call(messages=contra_repeat_message, **self.generation_model_kwargs)
+
+ # return if empty
+ if not response.status or not response.message.content:
+ return
+ response_text = response.message.content
+
+ # parse text
+ idx_merge_obs_list = ResponseTextParser(response_text, self.language, self.__class__.__name__).parse_v1()
+ if len(idx_merge_obs_list) <= 0:
+ self.logger.warning("idx_merge_obs_list is empty!")
+ return
+
+ # add merged obs
+ merge_obs_nodes: List[MemoryNode] = []
+ for obs_content_list in idx_merge_obs_list:
+ if not obs_content_list:
+ continue
+
+ # Expecting a pair [index, flag]
+ if len(obs_content_list) != 2:
+ self.logger.warning(f"obs_content_list={obs_content_list} is invalid!")
+ continue
+
+ idx, keep_flag = obs_content_list
+
+ if not idx.isdigit():
+ self.logger.warning(f"idx={idx} is invalid!")
+ continue
+
+ # index number needs to be corrected to -1
+ idx = int(idx) - 1
+ if idx >= len(all_obs_nodes):
+ self.logger.warning(f"idx={idx} is invalid!")
+ continue
+
+ # judge flag
+ keep_flag = keep_flag.lower()
+ if keep_flag not in self.get_language_value([NONE_WORD, CONTRADICTORY_WORD, CONTAINED_WORD]):
+ self.logger.warning(f"keep_flag={keep_flag} is invalid!")
+ continue
+
+ node: MemoryNode = all_obs_nodes[idx]
+ if keep_flag != self.get_language_value(NONE_WORD):
+ node.store_status = StoreStatusEnum.EXPIRED.value
+ self.logger.info(f"contra_repeat stage: {node.content} {node.store_status} {node.action_status}")
+ merge_obs_nodes.append(node)
+
+ # save context
+ self.memory_manager.set_memories(MERGE_OBS_NODES, merge_obs_nodes, log_repeat=False)
diff --git a/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.yaml b/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.yaml
new file mode 100644
index 00000000..3be7d3f8
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/contra_repeat_worker.yaml
@@ -0,0 +1,126 @@
+contra_repeat_system:
+ cn: |
+ 任务:对下面的{num_obs}句句子,逐一判断是否与“前面序号”的任意句子存在信息的矛盾,或者句子的主要信息被“前面序号”的任意句子中的信息包含。
+ 注意:对每句句子,只判断与“前面序号”的句子的关系,不要判断与“后面序号”的句子的关系。
+ 其中矛盾的形式可以有很多种,可以是逻辑上的矛盾,可以是属性上的变化导致的矛盾,比如不能同时在两个地方工作,同一个时刻不能在两个地点,同一个时刻不能干两件事情等等。
+ 对每个句子都做一个判断,最后一共输出{num_obs}条判断。
+ 请一步步思考,并按如下格式输出:
+ 思考:思考的依据和过程,30字以内。
+ 判断:<句子序号> <矛盾,被包含,无>,一定加<>
+
+ en: |
+ Task: For the following {num_obs} sentences, determine whether each sentence contradicts any of the previous numbered sentences or if the main information in the sentence is contained within the information from any of the previous numbered sentences.
+ Note: Only determine the relationship with the "previous numbered" sentences, do not judge the "later numbered" sentences.
+ The forms of contradiction can be varied. It can be a logical contradiction, or a contradiction due to changes in attributes, for example, not being able to work in two places at once, not being able to be in two places at the same time, not being able to do two things at the same time, etc.
+ Make a judgment for each sentence and output a total of {num_obs} judgments.
+ Think step by step and output in the following format:
+ Thought: Basis and process of thinking, within 30 words.
+ Judgment: , must be enclosed in <>.
+
+contra_repeat_few_shot:
+ cn: |
+ 示例1
+ 句子:
+ 1 {user_name}经常失眠,对安眠药的效果感兴趣,暗示可能考虑使用。
+ 2 {user_name}经常失眠,寻求缓解方法。
+ 3 陈伟业是{user_name}的领导
+ 4 陈伟业是{user_name}的领导
+ 5 陈伟业是{user_name}的领导,是银行分行行长
+ 6 {user_name}喜欢吃西瓜
+ 7 {user_name}喜欢吃苹果
+
+ 思考:第1句不会存在与前面序号句子的矛盾或者完全重复。
+ 判断:<1> <无>
+ 思考:第2句中所有信息都被前面序号中第1句的信息完全包含。
+ 判断:<2> <被包含>
+ 思考:第3句信息没有在前面序号句子中出现
+ 判断:<3> <无>
+ 思考:第4句与前面序号中第3句的信息完全重复,即被完全包含。
+ 判断:<4> <被包含>
+ 思考:第5句中陈伟业是{user_name}的领导的信息被前面序号中第3句的信息包含,但新增了陈伟业是银行分行行长的信息,故不是被完全包含。
+ 判断:<5> <无>
+ 思考:第6句中表达了{user_name}的水果偏好,喜欢吃西瓜,信息没有在前面序号句子中出现。
+ 判断:<6> <无>
+ 思考:第7句也表达了{user_name}的水果偏好,喜欢吃桃子,和前面序号中的第6句不冲突,喜好可以同时存在。
+ 判断:<7> <无>
+
+ 示例2
+ 句子:
+ 1 {user_name}的孩子成绩不太好。
+ 2 {user_name}的孩子在学校经常逃课。
+ 3 {user_name}的父亲生日在2024年6月2日,{user_name}打算准备礼物。
+ 4 {user_name}的父亲生日在2024年5月1日。
+ 5 {user_name}很喜欢和同班同学打篮球。
+ 6 {user_name}喜欢打篮球。
+
+ 思考:第1句不会存在与前面序号句子的矛盾或者完全重复。
+ 判断:<1> <无>
+ 思考:第2句与前面序号句子既不矛盾也不重复。
+ 判断:<2> <无>
+ 思考:第3句与前面序号句子既不矛盾也不重复。
+ 判断:<3> <无>
+ 思考:第4句关于{user_name}父亲生日的日期信息与前面序号句子第3句矛盾了。
+ 判断:<4> <矛盾>
+ 思考:第5句与前面序号句子既不矛盾也不重复。
+ 判断:<5> <无>
+ 思考:第6句中所有信息都被前面序号中第5句的信息完全包含。
+ 判断:<2> <被包含>
+
+ en: |
+ Example 1
+ Sentences:
+ 1 {user_name} suffers from insomnia frequently and is interested in the effects of sleeping pills, suggesting a possible consideration of their use.
+ 2 {user_name} suffers from insomnia frequently and seeks remedies.
+ 3 Charles is {user_name}'s supervisor.
+ 4 Charles is {user_name}'s supervisor.
+ 5 Charles is {user_name}'s supervisor and the branch manager of a bank.
+ 6 {user_name} loves playing basketball with classmates.
+ 7 {user_name} likes playing basketball.
+
+ Thought: The first sentence does not have any contradictions or complete repetitions with the previously numbered sentences.
+ Judgment: <1>
+ Thought: All information in the second sentence is completely contained within the information of the first sentence.
+ Judgment: <2>
+ Thought: The information in the third sentence does not appear in the previously numbered sentences.
+ Judgment: <3>
+ Thought: The fourth sentence is completely repetitive of the information in the third sentence, i.e., it is completely contained.
+ Judgment: <4>
+ Thought: The information that Charles is {user_name}'s supervisor in the fifth sentence is contained within the information of the third sentence, but the new information that Charles is the branch manager of a bank is not, so it is not contained.
+ Judgment: <5>
+ Thought: Sentence 6 expresses {user_name}'s fruit preference, liking to eat watermelon, which is information not present in any preceding sentences.
+ Judgment: <6>
+ Thought: Sentence 7 also expresses {user_name}'s fruit preference, liking to eat apples; it does not conflict with sentence 6, and both preferences can coexist.
+ Judgment: <7>
+
+ Example 2
+ Sentences:
+ 1 {user_name}'s child does not perform well academically.
+ 2 {user_name}'s child often skips school.
+ 3 {user_name}'s father's birthday is on June 2, 2024, and {user_name} plans to prepare a gift.
+ 4 {user_name}'s father's birthday is on May 1, 2024.
+ 5 {user_name} loves playing basketball with classmates.
+ 6 {user_name} likes playing basketball.
+
+ Thought: The first sentence does not have any contradictions or complete repetitions with the previously numbered sentences.
+ Judgment: <1>
+ Thought: The second sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <2>
+ Thought: The third sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <3>
+ Thought: The date of {user_name}'s father's birthday in the fourth sentence contradicts the information in the third sentence.
+ Judgment: <4>
+ Thought: The fifth sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <5>
+ Thought: All information in the sixth sentence is completely contained within the information of the fifth sentence.
+ Judgment: <6>
+
+
+
+contra_repeat_user_query:
+ cn: |
+ 句子:
+ {user_query}
+
+ en: |
+ Sentences:
+ {user_query}
diff --git a/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.py b/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.py
new file mode 100644
index 00000000..4bea129a
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.py
@@ -0,0 +1,76 @@
+from typing import List
+
+from memoryscope.constants.common_constants import NEW_OBS_WITH_TIME_NODES
+from memoryscope.constants.language_constants import COLON_WORD
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.worker.backend.get_observation_worker import GetObservationWorker
+from memoryscope.scheme.message import Message
+
+
+class GetObservationWithTimeWorker(GetObservationWorker):
+ """
+ A specialized worker class that extends GetObservationWorker functionality to handle
+ retrieval of observations which include associated timestamp information from chat messages.
+ """
+ FILE_PATH: str = __file__
+ OBS_STORE_KEY: str = NEW_OBS_WITH_TIME_NODES
+
+ def filter_messages(self) -> List[Message]:
+ """
+ Filters the chat messages to only include those which contain time-related keywords.
+
+ Returns:
+ List[Message]: A list of filtered messages that mention time.
+ """
+ filter_messages = []
+ for msg in self.chat_messages_scatter:
+ # Checks if the message content has any time reference words
+ if DatetimeHandler.has_time_word(query=msg.content, language=self.language):
+ filter_messages.append(msg)
+ return filter_messages
+
+ def build_message(self, filter_messages: List[Message]) -> List[Message]:
+ """
+ Constructs a prompt message for obtaining observations with timestamp information
+ based on filtered chat messages.
+
+ This method processes each filtered message with the timestamp information.
+ It then organizes these timestamped messages into a structured prompt that includes a system prompt,
+ few-shot examples, and the concatenated user queries.
+
+ Args:
+ filter_messages (List[Message]): A list of Message objects that have been filtered for processing.
+
+ Returns:
+ List[Message]: A list containing the newly constructed Message object for further interaction.
+ """
+ user_query_list = []
+ for i, msg in enumerate(filter_messages):
+ # Create a DatetimeHandler instance for each message's timestamp and format it
+ dt_handler = DatetimeHandler(dt=msg.time_created)
+ dt = dt_handler.string_format(string_format=self.prompt_handler.time_string_format, language=self.language)
+ # Append formatted timestamp-query pairs to the user_query_list
+ user_query_list.append(f"{i + 1} {dt} {self.target_name}{self.get_language_value(COLON_WORD)}{msg.content}")
+
+ # Construct the system prompt with the count of observations
+ system_prompt = self.prompt_handler.get_observation_with_time_system.format(num_obs=len(user_query_list),
+ user_name=self.target_name)
+
+ # Retrieve the few-shot examples for the prompt
+ few_shot = self.prompt_handler.get_observation_with_time_few_shot.format(user_name=self.target_name)
+
+ # Format the user query section with the concatenated list of timestamped queries
+ user_query = self.prompt_handler.get_observation_with_time_user_query.format(
+ user_query="\n".join(user_query_list),
+ user_name=self.target_name)
+
+ # Assemble the final message for observation retrieval
+ get_observation_message_wt = self.prompt_to_msg(system_prompt=system_prompt,
+ few_shot=few_shot,
+ user_query=user_query)
+
+ # Log the constructed message for debugging purposes
+ self.logger.info(f"get_observation_message_wt={get_observation_message_wt}")
+
+ # Return the newly created message
+ return get_observation_message_wt
diff --git a/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.yaml b/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.yaml
new file mode 100644
index 00000000..97320c23
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_observation_with_time_worker.yaml
@@ -0,0 +1,156 @@
+time_string_format:
+ cn: |
+ {year}年{month}{day}日{weekday}{hour}点
+ en: |
+ {month} {day}, {year}, {weekday}, at {hour}
+
+get_observation_with_time_system:
+ cn: |
+ 任务:从下面的{num_obs}句{user_name}句子中依次提取出关于{user_name}的重要信息,相应的关键词与时间信息。如果没有重要信息则回答“无”,最多提取{num_obs}条信息。
+ 每一句{user_name}句子的格式是:<序号> <对话时间> {user_name}:<句子>
+ {user_name}的重要信息可以包含用户基本信息,用户画像信息,用户兴趣偏好信息,用户性格,用户价值观,用户人际关系,用户重大事件转折点等等重要信息。
+ 如果句子中只包含{user_name}假设的信息或者{user_name}虚构的内容比如{user_name}创作的小说或剧本,回答“无”。
+ 如果{user_name}信息涉及时间,则结合对话时间推断{user_name}信息的时间信息,没有则不输出。
+ 对每个句子都做一次信息提取,最后一共输出{num_obs}条信息。
+ 请一步步思考,并一定要按如下格式依次输出,最后的结果一定要加<>:
+ 思考:思考的依据和过程,50字以内。
+ 信息:<句子序号> <时间信息或不输出> <明确的重要信息或“无”> <关键词>
+
+ en: |
+ Task: Extract important information about {user_name} from the following {num_obs} sentences of {user_name}, including relevant keywords and time information. If there is no important information, answer "none", with a maximum of {num_obs} pieces of information extracted.
+ Each sentence from {user_name} is formatted as follows: {user_name}: .
+ Important information about {user_name} can include basic information, user profile information, interest preferences, personality, values, human relationships, significant life events, etc.
+ If a sentence only contains hypothetical information or fictional content created by {user_name} (e.g., novels or scripts), answer "none".
+ If {user_name}'s information involves time, infer the time information based on the conversation time; if not, do not output.
+ Analyze each sentence once to extract information and output a total of {num_obs} pieces of information.
+ Please think step-by-step and be sure to output in the following format, with the final results enclosed in <>:
+ Thought: Basis and process of thought, within 50 words.
+ Information:
+
+
+get_observation_with_time_few_shot:
+ cn: |
+ 示例1:
+ {user_name}句子:
+ 1 2022年5月1日周二3点 {user_name}:帮我写一段给同事张三女儿三岁生日的祝福语。
+ 2 2022年5月2日周二17点 {user_name}:公元1400年至1550年中国历史大事表。
+ 3 2022年5月3日周二18点 {user_name}:能给我整理一张如何使用大模型的技巧列表吗,要求内容尽量精简。
+ 4 2022年7月3日周四12点 {user_name}:上上个月我办了游泳卡。
+
+ 思考:从第1句可以得知张三是{user_name}的同事,这是关于{user_name}的人际关系的重要信息。其余信息重要性不足。{user_name}信息不涉及时间。
+ 信息:<1> <> <张三是{user_name}的同事。> <张三, 同事>
+ 思考:第2句是{user_name}提出的要求,没有明确提及{user_name}个人信息。
+ 信息:<2> <> <无> <>
+ 思考:第3句是{user_name}提出的要求,没有明确提及{user_name}个人信息。
+ 信息:<3> <> <无> <>
+ 思考:从第4句可以得出{user_name}上上个月办了游泳卡。{user_name}信息涉及时间,结合对话时间为2022年7月,推断{user_name}在2022年5月{user_name}办了游泳卡。
+ 信息:<4> <2022年5月> <{user_name}在2022年5月办了游泳卡。> <游泳卡>
+
+
+ 示例2:
+ {user_name}句子:
+ 1 2020年1月4日周日10点 {user_name}:我花5000元买了100股海天味业。
+ 2 2023年4月27日周五8点 {user_name}:明天是我和妻子的结婚纪念日,帮我推荐一家餐厅。
+ 3 2020年1月4日周日10点 {user_name}:我花50000元买了100股阿里巴巴股票。
+ 4 2021年6月2日周四23点 {user_name}:谢啦。我中午在公司附近吃,帮我推荐一家阿里巴巴徐汇滨江园区附近的餐厅吧。
+ 5 2021年7月9日周六11点 {user_name}:两个坏消息,我打羽毛球把拍子打断线了。。。然后我去我朋友家撸猫,结果我猫毛过敏,今天疯狂打喷嚏。。。
+
+ 思考:从第1句可以得知{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元,这是关于{user_name}的投资决策的重要信息。{user_name}信息不涉及时间。
+ 信息:<1> <> <{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元。> <海天味业, 股票>
+ 思考:从第2句可以得知{user_name}与妻子的结婚纪念日是明天,这是关于{user_name}重要纪念日的信息。其余信息重要性不足。{user_name}信息涉及时间,结合对话时间为2023年4月27日,
+ 以及结婚纪念日为周期性日期,推断{user_name}与妻子的结婚纪念日是每年4月28日。
+ 信息:<2> <每年4月28日> <{user_name}与妻子的结婚纪念日是每年4月28日。> <妻子, 结婚纪念日>
+ 思考:第3句含有的信息与第1句相似,但是不重复,可以得知{user_name}购买了阿里巴巴股票。
+ 信息:<3> <> <{user_name}购买了阿里巴巴股票,购买数量为100股,购买金额为50000元。> <阿里巴巴, 股票>
+ 思考:从第4句以得知{user_name}在阿里巴巴徐汇滨江园区工作,这是关于{user_name}的工作的重要信息。其余信息重要性不足。{user_name}信息不涉及时间。
+ 信息:<4> <> <{user_name}在阿里巴巴徐汇滨江园区工作。> <阿里巴巴, 徐汇滨江园区, 工作>
+ 思考:从第5句可以得知{user_name}前天打羽毛球时把球拍打断了线,但这不是重要的信息。还可以得知{user_name}对猫毛过敏,这是关于{user_name}的健康的重要信息。{user_name}信息不涉及时间。
+ 信息:<5> <> <{user_name}对猫毛过敏。> <猫毛, 过敏>
+
+
+ 示例3:
+ {user_name}句子:
+ 1 2023年6月30日周五15点 {user_name}:上个月我和家人一起去杭州旅游,景色很不错。
+ 2 2023年7月2日周二10点 {user_name}:昨天是我生日,一个人过的。
+ 3 2020年7月3日周四11点 {user_name}:提醒我下周一去体检。
+ 4 2023年5月21日周六14点 {user_name}:有人说兴趣是最好的老师,也建议兴趣和职业联系起来,但我发现喜欢打篮球的人很多,但靠打篮球成职业的稀少,赚钱的更少,此外,怎么分辨兴趣和喜欢
+ 5 2018年3月6日周四19点 {user_name}:李增杰:这个是星座蛙设,但是我是处女座的,我妈感觉因为我的不正常,我妈不让我看了\n雌猴摸了摸李增杰的头,这样啊\n雌猴打开了哔哩哔哩看了看\n雌猴:要不换个设吧,我听你未来的你说,有一个叫难忘的朱古力232这个人,他弄的设是Windows设\n这是剧本1,剧本2未完待续
+
+ 思考:从第1句可以得知{user_name}和家人上个月去杭州旅游了,这是关于{user_name}的经历的重要信息。其余信息重要性不足。{user_name}信息涉及时间,结合对话时间为2023年6月推断{user_name}和家人2023年5月去杭州旅游了。
+ 信息:<1> <2023年5月> <{user_name}和家人2023年5月去杭州旅游了。> <家人, 杭州, 旅游>
+ 思考:从第2句可以得知{user_name}的生日是昨天,这是关于{user_name}重要纪念日的信息。其余信息重要性不足。{user_name}信息涉及时间,结合对话时间为2023年7月2日,
+ 以及生日为周期性日期,推断{user_name}的生日是每年7月2日。
+ 信息:<2> <每年7月2日> <{user_name}的生日是每年7月2日。> <生日>
+ 思考:从第3句可以得出{user_name}下周一去体检,这是{user_name}要求记忆的重要信息。{user_name}信息涉及时间,结合对话时间为2020年7月3日周四,推断{user_name}2020年7月6日周一去体检。
+ 信息:<3> <2020年7月6日周一> <{user_name}2020年7月6日周一去体检。> <体检>
+ 思考:第4句是{user_name}对他人观点的讨论和疑问,没有明确提及{user_name}个人信息。
+ 信息:<4> <> <无> <>
+ 思考:第5句是{user_name}创作的剧本内容,无法提取{user_name}个人信息。
+ 信息:<5> <> <无> <>
+
+ en: |
+ Example 1:
+ {user_name} sentences:
+ 1 May 1, 2022, Tuesday, at 3 {user_name}: Please help me write a birthday greeting for my colleague Jason's daughter who is turning three.
+ 2 May 2, 2022, Tuesday, at 17 {user_name}: Chronology of major events in Chinese history from 1400 to 1550 AD.
+ 3 May 3, 2022, Tuesday, at 18 {user_name}: Can you compile a list of tips on how to use large models for me, and try to keep the content concise?
+ 4 July 3, 2022, Thursday, at 12 {user_name}: I got a swimming pass two months ago.
+
+ Thought: From the first sentence, it can be inferred that Jason is {user_name}'s colleague, which is important information about {user_name}'s interpersonal relationships. The remaining information is of insufficient importance. {user_name}'s information does not involve time.
+ Information: <1> <>
+ Thought: The second sentence is a request made by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <2> <> <>
+ Thought: The third sentence is a request made by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <3> <> <>
+ Thought: From the fourth sentence, it can be inferred that {user_name} got a swimming pass two months ago. {user_name}'s information involves time. Combining it with the conversation time of July 2022, it can be inferred that {user_name} got the swimming pass in May 2022.
+ Information: <4> <{user_name} got a swimming pass in May 2022>
+
+ Example 2:
+ {user_name} sentences:
+ 1 January 4, 2020, Sunday, at 10 {user_name}: I spent $5000 to buy 100 shares of General Motors.
+ 2 April 27, 2023, Friday, at 8 {user_name}: Tomorrow is my wedding anniversary with my wife. Could you recommend a restaurant?
+ 3 January 4, 2020, Sunday, at 10 {user_name}: I spent $50000 to buy 100 shares of Alibaba.
+ 4 June 2, 2021, Thursday, at 23 {user_name}: Thanks. I'm having lunch near the company at noon; can you recommend a restaurant near Alibaba Xuhui Riverside Campus for me?
+ 5 July 9, 2021, Saturday, at 11 {user_name}: Two pieces of bad news: I broke my badminton racket while playing... Then I went to my friend's house to pet the cat and ended up having an allergic reaction to the cat fur, sneezing like crazy today...
+
+ Thought: From the first sentence, it can be inferred that {user_name} bought 100 shares of General Motors stock for $5000. This is important information about {user_name}'s investment decision. {user_name}'s information does not involve time.
+ Information: <1> <> <{user_name} bought 100 shares of General Motors stock for $5000>
+ Thought: From the second sentence, it can be inferred that {user_name}'s wedding anniversary with his wife is tomorrow, which is important information about {user_name}'s significant dates. The remaining information is of insufficient importance. {user_name}'s information involves time. Combining it with the conversation date of April 27, 2023, and knowing that the anniversary is a recurring date, it can be inferred that {user_name}'s wedding anniversary is on April 28th each year.
+ Information: <2> <{user_name}'s wedding anniversary with his wife is on April 28 each year>
+ Thought: The information in the third sentence is similar to, but not a repetition of the first sentence. It can be deduced that {user_name} purchased Alibaba stock.
+ Information: <3> <> <{user_name} purchased 100 shares of Alibaba stock for 50,000 RMB.>
+ Thought: From the fourth sentence, it can be inferred that {user_name} works at Alibaba Xuhui Riverside Campus, which is important information about {user_name}'s job. The remaining information is of insufficient importance. {user_name}'s information does not involve time.
+ Information: <4> <> <{user_name} works at Alibaba Xuhui Riverside Campus>
+ Thought: From the fifth sentence, it can be inferred that {user_name} broke their badminton racket the other day while playing, but this is not important information. It can also be inferred that {user_name} is allergic to cat fur, which is important information about {user_name}'s health. {user_name}'s information does not involve time.
+ Information: <5> <> <{user_name} is allergic to cat fur>
+
+
+ Example 3:
+ {user_name} sentences:
+ 1 June 30, 2023, Friday, at 15 {user_name}: Last month, my family and I went to San Jose for a trip. The scenery was very nice.
+ 2 July 2, 2023, Tuesday, at 10 {user_name}: Yesterday was my birthday. I spent it alone.
+ 3 July 3, 2020, Thursday, at 11 {user_name}: Remind me to go for a medical check-up next Monday.
+ 4 May 21, 2023, Saturday, at 14 {user_name}: Someone said that passion is the best teacher and suggested linking passion with a career, but I found that many people like playing basketball, but few make a career out of it, and even fewer make money from it. Also, how do you distinguish passion from liking?
+ 5 March 6, 2018, Thursday, at 19 {user_name}: Zack:This is a constellation frog setting, but I am a Virgo. My mom feels I am abnormal and doesn't let me watch it. \n The female monkey patted Zack's head, "Is that so?" \n The female monkey opened Bilibili and took a look. \n Female monkey: "Why don't you switch the setting? I heard from your future self that there's someone called 'Unforgettable Chocolate 232' who created a Windows setting." \n This is script 1; script 2 is to be continued.
+
+ Thought: From the first sentence, it can be inferred that {user_name} and their family went to San Jose for a trip last month. This is important information about {user_name}'s experience. The remaining information is of insufficient importance. {user_name}'s information involves time. Combining it with the conversation time of June 2023, it can be inferred that {user_name} and their family went to San Jose for a trip in May 2023.
+ Information: <1> <{user_name} and their family went to San Jose for a trip in May 2023>
+ Thought: From the second sentence, it can be inferred that {user_name}'s birthday was yesterday. This is important information about {user_name}'s significant dates. The remaining information is of insufficient importance. {user_name}'s information involves time. Combining it with the conversation time of July 2, 2023, and knowing that the birthday is a recurring date, it can be inferred that {user_name}'s birthday is on July 2 each year.
+ Information: <2> <{user_name}'s birthday is on July 2 each year>
+ Thought: From the third sentence, it can be inferred that {user_name} will go for a medical check-up next Monday, which is an important reminder for {user_name}. {user_name}'s information involves time. Combining it with the conversation time of July 3, 2020, Thursday, it can be inferred that {user_name} will go for a check-up on July 6, 2020, Monday.
+ Information: <3> <{user_name} will go for a medical check-up on July 6, 2020, Monday>
+ Thought: The fourth sentence is a discussion and query about other people's opinions by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <4> <> <>
+ Thought: The fifth sentence is content from a script written by {user_name}, with no extractable personal information about {user_name}.
+ Information: <5> <> <>
+
+
+get_observation_with_time_user_query:
+ cn: |
+ {user_name}句子:
+ {user_query}
+
+ en: |
+ {user_name} sentences:
+ {user_query}
+
diff --git a/memoryscope/memoryscope/core/worker/backend/get_observation_worker.py b/memoryscope/memoryscope/core/worker/backend/get_observation_worker.py
new file mode 100644
index 00000000..b43f8fca
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_observation_worker.py
@@ -0,0 +1,188 @@
+from typing import List
+
+from memoryscope.constants.common_constants import NEW_OBS_NODES, TIME_INFER
+from memoryscope.constants.language_constants import REPEATED_WORD, NONE_WORD, COLON_WORD, TIME_INFER_WORD
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.scheme.memory_node import MemoryNode
+from memoryscope.scheme.message import Message
+
+
+class GetObservationWorker(MemoryBaseWorker):
+ """
+ A specialized worker class to generate the observations from the original chat histories.
+ """
+ FILE_PATH: str = __file__
+ OBS_STORE_KEY: str = NEW_OBS_NODES
+
+ def _parse_params(self, **kwargs):
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+
+ def add_observation(self, message: Message, time_infer: str, obs_content: str, keywords: str):
+ """
+ Builds a MemoryNode containing the observation details.
+
+ Args:
+ message (Message): The source message from which the observation is derived.
+ time_infer (str): The inferred time if available.
+ obs_content (str): The content of the observation.
+ keywords (str): Keywords associated with the observation.
+
+ Returns:
+ MemoryNode: The constructed MemoryNode containing the observation.
+ """
+ dt_handler = DatetimeHandler(dt=message.time_created)
+
+ # build meta data
+ meta_data = {
+ MemoryTypeEnum.CONVERSATION.value: message.content,
+ TIME_INFER: time_infer,
+ "keywords": keywords,
+ **{k: str(v) for k, v in dt_handler.get_dt_info_dict(self.language).items()},
+ }
+
+ if time_infer:
+ dt_info_dict = DatetimeHandler.extract_date_parts(input_string=time_infer, language=self.language)
+ meta_data.update({f"event_{k}": str(v) for k, v in dt_info_dict.items()})
+ obs_content = (f"{obs_content} ({self.get_language_value(TIME_INFER_WORD)}"
+ f"{self.get_language_value(COLON_WORD)} {time_infer})")
+
+ return MemoryNode(user_name=self.user_name,
+ target_name=self.target_name,
+ meta_data=meta_data,
+ content=obs_content,
+ memory_type=MemoryTypeEnum.OBSERVATION.value,
+ action_status=ActionStatusEnum.NEW.value,
+ timestamp=message.time_created)
+
+ def filter_messages(self) -> List[Message]:
+ """
+ Filters the chat messages to only include those which not contain time-related keywords.
+
+ Returns:
+ List[Message]: A list of filtered messages that mention time.
+ """
+ filter_messages = []
+ for msg in self.chat_messages_scatter:
+ if not DatetimeHandler.has_time_word(query=msg.content, language=self.language):
+ filter_messages.append(msg)
+
+ self.logger.info(f"after filter_messages.size from {len(self.chat_messages_scatter)} to {len(filter_messages)}")
+ return filter_messages
+
+ def build_message(self, filter_messages: List[Message]) -> List[Message]:
+ """
+ Constructs a formatted message for observation based on input messages, incorporating system prompts,
+ few-shot examples, and user queries.
+
+ Args:
+ filter_messages (List[Message]): A list of messages filtered for observation processing.
+
+ Returns:
+ List[Message]: A list containing the constructed message ready for observation.
+ """
+ user_query_list = []
+ for i, msg in enumerate(filter_messages):
+ # Construct each user query item with index, target name, and message content
+ user_query_list.append(f"{i + 1} {self.target_name}{self.get_language_value(COLON_WORD)}{msg.content}")
+
+ # Format the system prompt with the number of observations and target name
+ system_prompt = self.prompt_handler.get_observation_system.format(num_obs=len(user_query_list),
+ user_name=self.target_name)
+
+ # Incorporate few-shot examples into the prompt with the target name
+ few_shot = self.prompt_handler.get_observation_few_shot.format(user_name=self.target_name)
+
+ # Assemble the user query part of the prompt with the list of formatted user queries
+ user_query = self.prompt_handler.get_observation_user_query.format(user_query="\n".join(user_query_list),
+ user_name=self.target_name)
+
+ # Combine system prompt, few-shot, and user query into a single message for obtaining observations
+ get_observation_message = self.prompt_to_msg(system_prompt=system_prompt,
+ few_shot=few_shot,
+ user_query=user_query)
+
+ # Log the constructed observation message
+ self.logger.info(f"get_observation_message={get_observation_message}")
+
+ # Return the processed message(s) for further steps in the observation workflow
+ return get_observation_message
+
+ def _run(self):
+ """
+ Processes chat messages to extract observations, inferring timestamps and content relevance,
+ and stores the extracted information as MemoryNode objects within the conversation memory.
+
+ Steps:
+ 1. Filter messages based on predefined criteria.
+ 2. Constructs a message for the language model to generate observations.
+ 3. Calls the language model to predict observation details.
+ 4. Parses the model's response to extract observation lists.
+ 5. Validates and structures each observed event into MemoryNode objects.
+ 6. Stores these MemoryNodes in the conversation memory under a specific key.
+ """
+ # Filters messages and constructs an input message for the language model
+ filter_messages = self.filter_messages()
+ if not filter_messages:
+ self.logger.warning("get obs filter_messages is empty!")
+ return
+
+ obtain_obs_message = self.build_message(filter_messages)
+
+ # Generates observations using the language model
+ response = self.generation_model.call(messages=obtain_obs_message, **self.generation_model_kwargs)
+ if not response.status or not response.message.content:
+ return
+
+ response_text = response.message.content
+
+ # Parses the generated text to extract observation indices, times, contents, and keywords
+ idx_obs_list = ResponseTextParser(response_text, self.language, self.__class__.__name__).parse_v1()
+ if len(idx_obs_list) <= 0:
+ self.logger.warning("idx_obs_list is empty!")
+ return
+
+ # Processes each extracted observation to create MemoryNode objects
+ new_obs_nodes: List[MemoryNode] = []
+ for obs_content_list in idx_obs_list:
+ if not obs_content_list:
+ continue
+
+ # Expected format: [index, time_inference, observation_content, keywords]
+ if len(obs_content_list) != 4:
+ self.logger.warning(f"obs_content_list={obs_content_list} is invalid!")
+ continue
+
+ idx, time_infer, obs_content, keywords = obs_content_list
+
+ # Skips processing if content indicates no meaningful observation
+ obs_content = obs_content.lower()
+ if obs_content in self.get_language_value([NONE_WORD, REPEATED_WORD]):
+ continue
+
+ # Validates index format
+ if not idx.isdigit():
+ self.logger.warning(f"idx={idx} is invalid!")
+ continue
+
+ time_infer = time_infer.lower()
+ if time_infer == self.get_language_value(NONE_WORD):
+ time_infer = ""
+
+ # Adjusts index to zero-based and checks validity against filtered messages
+ idx = int(idx) - 1
+ if idx >= len(filter_messages):
+ self.logger.warning(f"idx={idx} is invalid! filter_messages.size={len(filter_messages)}")
+ continue
+
+ # Creates a MemoryNode for the validated observation and adds it to the list
+ new_obs_nodes.append(self.add_observation(message=filter_messages[idx],
+ time_infer=time_infer,
+ obs_content=obs_content,
+ keywords=keywords))
+
+ # Stores the extracted and structured observations in the conversation memory
+ self.memory_manager.set_memories(self.OBS_STORE_KEY, new_obs_nodes)
diff --git a/memoryscope/memoryscope/core/worker/backend/get_observation_worker.yaml b/memoryscope/memoryscope/core/worker/backend/get_observation_worker.yaml
new file mode 100644
index 00000000..0ca5e0fc
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_observation_worker.yaml
@@ -0,0 +1,164 @@
+get_observation_system:
+ cn: |
+ 任务:从下面的{num_obs}句{user_name}句子中依次提取出关于{user_name}的重要信息,与相应的关键词。如果没有重要信息则回答“无”,最多提取{num_obs}条信息。
+ {user_name}的重要信息可以包含用户基本信息,用户画像信息,用户兴趣偏好信息,用户性格,用户价值观,用户人际关系,用户重大事件转折点等等重要信息。
+ 如果句子中只包含{user_name}假设的信息或者{user_name}虚构的内容比如{user_name}创作的小说或剧本,回答“无”。
+ 对每个句子都做一次信息提取,最后一共输出{num_obs}条信息。
+ 请一步步思考,并一定要按如下格式依次输出,最后的结果一定要加<>:
+ 思考:思考的依据和过程,50字以内。
+ 信息:<句子序号> <> <明确的重要信息或“无”> <关键词>
+
+ en: |
+ Task: Sequentially extract important information about {user_name} from the following {num_obs} sentences along with corresponding keywords. If there is no important information, answer "None". Extract up to {num_obs} pieces of information.
+ Important information about {user_name} can include basic information, user profile information, user interests and preferences, user personality, user values, user relationships, major turning points in the user's life, and other important information.
+ If the sentence only contains hypothetical information about {user_name} or fictional content created by {user_name} such as novels or scripts, respond "None".
+ Perform information extraction for each sentence, and output {num_obs} pieces of information in total.
+ Please think step-by-step, and be sure to output in the following format, ending with '<>':
+ Thought: Basis and process of thinking, within 50 words.
+ Information: <>
+
+
+get_observation_few_shot:
+ cn: |
+ 示例1:
+ {user_name}句子:
+ 1 {user_name}:我现在处境很糟,没有工作,负债几万,怎么办
+ 2 {user_name}:有人说兴趣是最好的老师,也建议兴趣和职业联系起来,但我发现喜欢打篮球的人很多,但靠打篮球成职业的稀少,赚钱的更少,此外,怎么分辨兴趣和喜欢
+ 3 {user_name}:我现在心情很糟糕
+ 4 {user_name}:我是一个刚毕业的学生,对社会,行业不了解,给我介绍一下社会系统和行业格局
+ 5 {user_name}:我花5000元买了100股海天味业。
+ 6 {user_name}:我花50000元买了100股阿里巴巴。
+ 思考:从第1句可以得知{user_name}现在没有工作,负债几万,这是关于{user_name}工作与经济状况的重要信息。
+ 信息:<1> <> <{user_name}当前无工作且负债几万> <无工作, 负债几万>
+ 思考:第2句是{user_name}对他人观点的讨论和疑问,没有明确提及{user_name}个人信息。
+ 信息:<2> <> <无> <>
+ 思考:从第3句可以得知{user_name}当前心情不好。
+ 信息:<3> <> <{user_name}当前心情不好> <心情>
+ 思考:从第4句可以得知{user_name}是一个刚毕业的学生,这是关于{user_name}身份背景状况的重要信息。其余信息重要性不足。
+ 信息:<4> <> <{user_name}是一名刚毕业的学生。> <刚毕业, 学生>
+ 思考:从第5句可以得知{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元,这是关于{user_name}的投资决策的重要信息。
+ 信息:<5> <> <{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元。> <海天味业, 股票>
+ 思考:第6句含有的信息与第1句相似,可以得知{user_name}购买了阿里巴巴股票。
+ 信息:<6> <> <{user_name}购买了阿里巴巴股票,购买数量为100股,购买金额为50000元。> <阿里巴巴, 股票>
+
+ 示例2:
+ {user_name}句子:
+ 1 {user_name}:帮我写一段给同事张三女儿三岁生日的祝福语。
+ 2 {user_name}:能给我整理一张如何使用大模型的技巧列表吗,要求内容尽量精简。
+ 3 {user_name}:两个坏消息,我打羽毛球把拍子打断线了。。。然后我去我朋友家撸猫,结果我猫毛过敏,今天疯狂打喷嚏。。。
+ 4 {user_name}:公元1400年至1550年中国历史大事表。
+ 5 {user_name}:谢啦。我中午在公司附近吃,帮我推荐一家阿里巴巴徐汇滨江园区附近的餐厅吧。
+ 思考:从第1句可以得知张三是{user_name}的同事,这是关于{user_name}的人际关系的重要信息。其余信息重要性不足。
+ 信息:<1> <> <张三是{user_name}的同事。> <张三, 同事>
+ 思考:第2句是{user_name}提出的要求,没有明确提及{user_name}个人信息。
+ 信息:<2> <> <无> <>
+ 思考:从第3句可以得知{user_name}前天打羽毛球时把球拍打断了线,但这不是重要的信息。还可以得知{user_name}对猫毛过敏,这是关于{user_name}的健康的重要信息。
+ 信息:<3> <> <{user_name}对猫毛过敏。> <猫毛, 过敏>
+ 思考:从第4句是{user_name}提出的要求,没有明确提及{user_name}个人信息。
+ 信息:<4> <> <无> <>
+ 思考:从第5句可以得知{user_name}在阿里巴巴徐汇滨江园区工作,这是关于{user_name}的工作地点的重要信息。
+ 信息:<5> <> <{user_name}在阿里巴巴徐汇滨江园区工作。> <阿里巴巴, 徐汇滨江园区, 工作>
+
+ 示例3:
+ {user_name}句子:
+ 1 {user_name}:我想买辆新能源汽车,有什么推荐吗?
+ 2 {user_name}:我在上海,想买辆新能源汽车,有什么推荐吗?
+ 3 {user_name}:案外人异议审查期间,人民法院不得对执行标的进行处分,不就是中止执行的意思吗?
+ 4 {user_name}:请写两句藏头诗分别以“胜”和“利”开头。
+ 5 {user_name}:我花5000元买了100股海天味业。
+ 6 {user_name}:李增杰:这个是星座蛙设,但是我是处女座的,我妈感觉因为我的不正常,我妈不让我看了\n雌猴摸了摸李增杰的头,这样啊\n雌猴打开了哔哩哔哩看了看\n雌猴:要不换个设吧,我听你未来的你说,有一个叫难忘的朱古力232这个人,他弄的设是Windows设\n这是剧本1,剧本2未完待续
+ 思考:从第1句可以得知{user_name}寻求购买新能源汽车的建议或推荐,这是这是关于{user_name}的大宗消费的重要的信息。
+ 信息:<1> <> <{user_name}寻求购买新能源汽车的建议或推荐。> <购买, 新能源汽车>
+ 思考:从第2句可以得知{user_name}当前所在城市为上海,这是关于{user_name}的生活地区的重要信息。
+ 信息:<2> <> <{user_name}所在的城市是上海。> <上海>
+ 思考:第3句是{user_name}对某个观点的讨论和疑问,没有明确提及{user_name}个人信息。
+ 信息:<3> <> <无> <>
+ 思考:第4句是{user_name}提出的要求,没有明确提及{user_name}个人信息。
+ 信息:<4> <> <无> <>
+ 思考:从第5句可以得知{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元,这是关于{user_name}的投资决策的重要信息。
+ 信息:<5> <> <{user_name}购买了海天味业股票,购买数量为100股,购买金额为5000元。> <海天味业, 股票>
+ 思考:第6句是{user_name}创作的剧本内容,无法提取{user_name}个人信息。
+ 信息:<6> <> <无> <>
+
+ 示例4:
+ {user_name}句子:
+ 1 {user_name}:李子好酸啊,我不太喜欢吃。
+ 2 {user_name}:桃子上的毛太多了,我不爱吃他。
+ 思考:从第1句可以得知{user_name}不太喜欢吃李子。
+ 信息:<1> <> <{user_name}不喜欢吃李子。> <李子>
+ 思考:从第2句可以得知{user_name}不喜欢吃桃子,和上一句相似都是对某一种水果不喜欢,但是表达了不同的信息。
+ 信息:<2> <> <{user_name}不喜欢吃桃子。> <西瓜>
+
+
+ en: |
+ Example 1:
+ {user_name} sentences:
+ 1 {user_name}: I'm in a terrible situation right now, I don't have a job, and I'm in debt by tens of thousands. What should I do?
+ 2 {user_name}: Someone said that passion is the best teacher and suggested linking passion with a career, but I found that many people like playing basketball, but few make it a profession and even fewer make money from it. Also, how do you distinguish passion from liking?
+ 3 {user_name}: I'm in a terrible situation right now, I don't have a job, and I'm in debt by tens of thousands. What should I do?
+ 4 {user_name}: I'm a recent graduate who doesn't understand society or the industry. Can you introduce me to the social system and industry structure?
+ 5 {user_name}: I spent $5000 to buy 100 shares of General Motors.
+ 6 {user_name}: I spent $50000 to buy 100 shares of Alibaba.
+
+ Thought: From the first sentence, it can be inferred that {user_name} currently has no job and is in debt by tens of thousands. This is important information about {user_name}'s employment and financial status.
+ Information: <1> <> <{user_name} currently has no job and is in debt by tens of thousands>
+ Thought: The second sentence is a discussion and query about others' opinions by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <2> <> <>
+ Thought: The information in the third sentence is a repeat of the first sentence.
+ Information: <3> <> <>
+ Thought: From the fourth sentence, it can be inferred that {user_name} is a recent graduate, which is important information about {user_name}'s background. The remaining information is of insufficient importance.
+ Information: <4> <> <{user_name} is a recent graduate>
+ Thought: It can be inferred that {user_name} bought 100 shares of General Motors stock for $5000. This is important information about {user_name}'s investment decision.
+ Information: <5> <> <{user_name} bought 100 shares of General Motors stock for $5000>
+ Thought: The information of the sentence is similar to, but not a repetition of the sentence before. It can be deduced that {user_name} purchased Alibaba stock.
+ Information: <6> <> <{user_name} purchased 100 shares of Alibaba stock for 50,000 RMB.>
+
+ Example 2:
+ {user_name} sentences:
+ 1 {user_name}: Please help me write a birthday greeting for my colleague Jason's daughter who is turning three.
+ 2 {user_name}: Can you compile a list of tips on how to use large models for me, and try to keep the content concise?
+ 3 {user_name}: Two pieces of bad news: I broke my badminton racket while playing... Then I went to my friend's house to pet the cat and ended up having an allergic reaction to the cat fur, sneezing like crazy today...
+ 4 {user_name}: Chronology of major events in Chinese history from 1400 to 1550 AD.
+ 5 {user_name}: Thanks. I'm having lunch near the company at noon; can you recommend a restaurant near Alibaba Xuhui Riverside Campus for me?
+ Thought: From the first sentence, it can be inferred that Zhang San is {user_name}'s colleague, which is important information about {user_name}'s interpersonal relationships. The remaining information is of insufficient importance.
+ Information: <1> <>
+ Thought: The second sentence is a request made by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <2> <> <>
+ Thought: From the third sentence, it can be inferred that {user_name} broke their badminton racket the other day, but this is not important information. It can also be inferred that {user_name} is allergic to cat fur, which is important information about {user_name}'s health.
+ Information: <3> <> <{user_name} is allergic to cat fur>
+ Thought: The fourth sentence is a request made by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <4> <> <>
+ Thought: From the fifth sentence, it can be inferred that {user_name} works at Alibaba Xuhui Riverside Campus, which is important information about {user_name}'s workplace.
+ Information: <5> <> <{user_name} works at Alibaba Xuhui Riverside Campus>
+
+ Example 3:
+ {user_name} sentences:
+ 1 {user_name}: I want to buy a new energy vehicle. Any recommendations?
+ 2 {user_name}: I'm in San Jose and want to buy a new energy vehicle. Any recommendations?
+ 3 {user_name}: During the objection review period by a third party, the court must not dispose of the execution object. Doesn't this mean suspension of execution?
+ 4 {user_name}: Please write two acrostic poems, starting with "Victory" and "Success".
+ 5 {user_name}: I spent $5000 to buy 100 shares of General Motors.
+ 6 {user_name}: Zack: This is a constellation frog setting, but I am a Virgo. My mom feels I'm abnormal and doesn't let me watch it. \n The female monkey patted Zack's head, "Is that so?" \n The female monkey opened Bilibili and took a look. \n Female monkey: "Why don't you switch the setting? I heard from your future self that there is someone called 'Unforgettable Chocolate 232' who created a Windows setting." \n This is script 1; script 2 is to be continued.
+ Thought: From the first sentence, it can be inferred that {user_name} is seeking advice or recommendations for purchasing a new energy vehicle. This is important information about {user_name}'s major consumption.
+ Information: <1> <> <{user_name} is seeking advice or recommendations for purchasing a new energy vehicle>
+ Thought: From the second sentence, it can be inferred that {user_name} is currently in San Jose, which is important information about {user_name}'s living location. The remaining information is a repeat of the first sentence.
+ Information: <2> <> <{user_name} is currently in San Jose>
+ Thought: The third sentence is a discussion and query about a specific legal opinion by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <3> <> <>
+ Thought: The fourth sentence is a request made by {user_name}, with no clear mention of {user_name}'s personal information.
+ Information: <4> <> <>
+ Thought: From the fifth sentence, it can be inferred that {user_name} bought 100 shares of General Motors stock for $5000. This is important information about {user_name}'s investment decision.
+ Information: <5> <> <{user_name} bought 100 shares of General Motors stock for $5000>
+ Thought: The sixth sentence is content from a script written by {user_name}, with no extractable personal information about {user_name}.
+ Information: <6> <> <>
+
+
+get_observation_user_query:
+ cn: |
+ {user_name}句子:
+ {user_query}
+
+ en: |
+ {user_name} sentences:
+ {user_query}
+
diff --git a/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.py b/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.py
new file mode 100644
index 00000000..aa24a833
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.py
@@ -0,0 +1,112 @@
+from typing import List
+
+from memoryscope.constants.common_constants import NOT_REFLECTED_NODES, INSIGHT_NODES
+from memoryscope.constants.language_constants import COMMA_WORD
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class GetReflectionSubjectWorker(MemoryBaseWorker):
+ """
+ A specialized worker class responsible for retrieving unreflected memory nodes,
+ generating reflection prompts with current insights, invoking an LLM for fresh insights,
+ parsing the LLM responses, forming new insight nodes, and updating memory statuses accordingly.
+ """
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.reflect_obs_cnt_threshold: int = kwargs.get("reflect_obs_cnt_threshold", 10)
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+ self.reflect_num_questions: int = kwargs.get("reflect_num_questions", 1)
+
+ def new_insight_node(self, insight_key: str) -> MemoryNode:
+ """
+ Creates a new MemoryNode for an insight with the given key, enriched with current datetime metadata.
+
+ Args:
+ insight_key (str): The unique identifier for the insight.
+
+ Returns:
+ MemoryNode: A new MemoryNode instance representing the insight, marked as new and of type INSIGHT.
+ """
+ dt_handler = DatetimeHandler()
+ # Prepare metadata with current datetime info
+ meta_data = {k: str(v) for k, v in dt_handler.get_dt_info_dict(self.language).items()}
+
+ return MemoryNode(user_name=self.user_name,
+ target_name=self.target_name,
+ meta_data=meta_data,
+ key=insight_key,
+ memory_type=MemoryTypeEnum.INSIGHT.value,
+ action_status=ActionStatusEnum.NEW.value)
+
+ def _run(self):
+ """
+ Executes the main logic of reflecting on unaudited memory nodes to derive new insights.
+
+ Steps include:
+ - Retrieving unaudited memory nodes.
+ - Checking the count against a threshold to decide whether to proceed.
+ - Compiling a list of existing insight keys.
+ - Generating a reflection prompt with system message, few-shot examples, and user queries.
+ - Calling the language model for new insights.
+ - Parsing the model's responses for new insight keys.
+ - Creating new insight nodes and updating the memory status accordingly.
+ """
+ not_reflected_nodes: List[MemoryNode] = self.memory_manager.get_memories(NOT_REFLECTED_NODES)
+ insight_nodes: List[MemoryNode] = self.memory_manager.get_memories(INSIGHT_NODES)
+
+ # Count unaudited nodes
+ not_reflected_count = len(not_reflected_nodes)
+ if not_reflected_count < self.reflect_obs_cnt_threshold:
+ self.logger.info(f"not_reflected_count({not_reflected_count}) < threshold({self.reflect_obs_cnt_threshold})"
+ f" is not enough, skip.")
+ # self.continue_run = False
+ return
+
+ # Compile existing insight keys
+ exist_keys: List[str] = [n.key for n in insight_nodes]
+ self.logger.info(f"exist_keys={exist_keys}")
+
+ # Generate reflection prompt components
+ user_query_list = [n.content for n in not_reflected_nodes]
+ if self.reflect_num_questions > 0:
+ num_questions = self.reflect_num_questions
+ else:
+ num_questions = len(user_query_list)
+
+ system_prompt = self.prompt_handler.get_reflection_subject_system.format(
+ user_name=self.target_name,
+ num_questions=num_questions)
+ few_shot = self.prompt_handler.get_reflection_subject_few_shot.format(user_name=self.target_name)
+ user_query = self.prompt_handler.get_reflection_subject_user_query.format(
+ user_name=self.target_name,
+ exist_keys=self.get_language_value(COMMA_WORD).join(exist_keys),
+ user_query="\n".join(user_query_list))
+
+ # Construct and log reflection message
+ reflect_message = self.prompt_to_msg(system_prompt=system_prompt, few_shot=few_shot, user_query=user_query)
+ self.logger.info(f"reflect_message={reflect_message}")
+
+ # Invoke Language Model for new insights
+ response = self.generation_model.call(messages=reflect_message, **self.generation_model_kwargs)
+
+ # Handle empty response
+ if not response.status or not response.message.content:
+ return
+
+ # Parse LLM response for new insight keys and update memory
+ new_insight_keys = ResponseTextParser(response.message.content, self.language,
+ self.__class__.__name__).parse_v2()
+ if new_insight_keys:
+ for insight_key in new_insight_keys:
+ self.memory_manager.add_memories(INSIGHT_NODES, self.new_insight_node(insight_key))
+
+ # Mark unaudited nodes as reflected
+ for node in not_reflected_nodes:
+ node.obs_reflected = 1
+ node.action_status = ActionStatusEnum.MODIFIED
diff --git a/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.yaml b/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.yaml
new file mode 100644
index 00000000..53a8d9b4
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/get_reflection_subject_worker.yaml
@@ -0,0 +1,179 @@
+get_reflection_subject_system:
+ cn: |
+ 任务:从下面的信息中提取出最重要的最多{num_questions}条{user_name}属性,要求不与已有的{user_name}属性语义重复。
+ 要求1:{user_name}属性可以是基本信息,基础画像,也可以是运动偏好,旅游偏好,饮食偏好等等兴趣偏好,也可以是重要事件性质,比如最近重要的事情,也可以是一些高度概括的人生理想,价值观,人生观,性格,也可以是和朋友的人际关系等等。
+ 要求2:根据{user_name}属性,我们可以生成“{user_name}的<{user_name}属性>是什么?”的问题,以此可以从下面的信息中提取{user_name}属性对应的值。
+ 输出格式:每一行输出一个{user_name}属性,每个{user_name}属性推荐4个字,如果没有信息请回答无,最多输出{num_questions}条。
+ en: |
+ Task: Extract the most important information from the following and provide up to {num_questions} attributes for {user_name}, ensuring that they do not semantically duplicate the existing attributes of {user_name}.
+ Requirement 1: {user_name} attributes can be basic information, basic profile descriptions, sports preferences, travel preferences, dietary preferences, interests, important life events, highly generalized life goals, values, life views, personality, interpersonal relationships with friends, etc.
+ Requirement 2: Based on the {user_name} attributes, we should be able to generate a question in the form of “What is {user_name}'s ?” From this, we can extract the corresponding values for the {user_name} attributes from the information below.
+ Output format: Output one {user_name} attribute per line, each attribute suggested to be 4 characters. If there is no information, please respond with "None". Output up to {num_questions} attributes.
+
+get_reflection_subject_few_shot:
+ cn: |
+ 示例1
+ 信息:
+ {user_name}想知道明天上海的天气情况。
+ {user_name}可能在上海工作,并关心是否需要带伞上班。
+ {user_name}在阿里巴巴徐汇滨江园区附近工作。
+ {user_name}计划中午在公司附近用餐。
+ {user_name}对咖啡因过敏。
+ {user_name}喝了咖啡后晚上会出现失眠的情况。
+ {user_name}偏好口味较为清淡、不辣的中餐馆。
+ {user_name}刚开始了他们的第一份工作。
+ {user_name}的工作岗位是阿里巴巴的算法工程师。
+ {user_name}希望得到与该岗位相关的职场建议。
+ {user_name}面临的问题是在项目进展初期如何有效与上司沟通。
+ {user_name}的目标是及时同步项目状态给上司。
+ {user_name}希望了解image generation(图像生成)技术的发展概览和最新进展。
+ {user_name}对variational auto-encoder、GAN、Diffusion Model等技术及其相互关系感兴趣。
+ 已有{user_name}属性:性别,工作地点,工作单位,睡眠状况,美食偏好
+ 新增{user_name}属性:
+ 过敏源
+ 技术方向
+ 工作岗位
+
+
+ 示例2
+ 信息:
+ {user_name}想要了解如何使用torchvision库来可视化深度学习任务的进度信息。
+ {user_name}希望了解如何将基于numpy和pytorch的并行计算方案迁移到CUDA支持的GPU上运行。
+ {user_name}询问是否需要依赖特定的包来完成这一任务。
+ {user_name}希望了解如何在Python中自定义进程和线程以实现并行计算。
+ {user_name}在编程中遇到了与并行计算相关的问题。
+ {user_name}希望学习如何使用Python(numpy,pytorch)在GPU上实现简单的并行计算。
+ {user_name}希望了解并行计算的基本概念,包括threads。
+ {user_name}询问有关世界各地著名菜系的信息。
+ {user_name}对全球各地的美食非常感兴趣。
+ {user_name}关心其体重与运动消耗的额外热量及心率之间的关系。
+ {user_name}在询问为了实现这一目标,每天需要额外消耗多少大卡热量。
+ {user_name}希望每月减重1kg。
+ {user_name}希望得到类似战略类手机游戏的推荐。
+ {user_name}喜欢玩三国志系列、文明系列、全面战争、骑马与砍杀等战略类游戏。
+ {user_name}希望根据他们的喜好获得新的游戏推荐。
+ {user_name}列举了他们喜欢的具体游戏类型,包括:三国志系列、文明系列、全面战争、骑马与砍杀等。
+ {user_name}喜欢玩战略类游戏。
+ 已有{user_name}属性:工作地点,性别,美食偏好
+ 新增{user_name}属性:
+ 游戏偏好
+ 运动计划
+ 技术方向
+
+ 示例3
+ 信息:
+ {user_name}寻求推荐一个相关课程或网址以进行学习。
+ {user_name}计划去青岛旅游。
+ {user_name}正为张三的女儿选购生日礼物。
+ {user_name}请求为一位名叫张三的人的女儿撰写一段温馨的祝福语。
+ {user_name}的同事名叫张三。
+ {user_name}与张三约定讨论阿里云百炼项目。
+ {user_name}与同事张三讨论了该项目的PRD(产品需求文档)。
+ 同事张三计划下周对PRD进行最终确定。
+ 张三还安排了在再下一周进行POC(Proof of Concept,概念验证)的讨论。
+ {user_name}希望获知该项目工程开发工作的负责团队信息,以了解项目执行的组织架构与分工情况。
+ 已有{user_name}属性:
+ 新增{user_name}属性:
+ 朋友关系
+
+ 示例4
+ 信息:
+ {user_name}在寻求有关推拿按摩手法的教程或相关网站推荐。
+ {user_name}希望系统地学习正规的推拿按摩手法。
+ {user_name}对按摩感兴趣,并且经常去推拿按摩店。
+ {user_name}想了解自己在静息状态下一小时大概会消耗多少大卡热量。
+ {user_name}年龄为28岁。
+ {user_name}体重为70kg。
+ {user_name}是男性。
+ 已有{user_name}属性:性别,体重,当前学习进展
+ 新增{user_name}属性:
+ 年龄
+
+ en: |
+ Example 1
+ Information:
+ {user_name} wants to know the weather forecast for Shanghai tomorrow.
+ {user_name} might be working in Shanghai and is concerned about whether they need to bring an umbrella to work.
+ {user_name} works near the Alibaba Xuhui Riverside Park office.
+ {user_name} plans to have lunch near the office.
+ {user_name} is allergic to caffeine.
+ {user_name} experiences insomnia at night after drinking coffee.
+ {user_name} prefers mild-flavored, non-spicy Chinese restaurants.
+ {user_name} just started their first job.
+ {user_name} works as an algorithm engineer at Alibaba.
+ {user_name} is seeking workplace advice related to this role.
+ {user_name} is facing the challenge of effectively communicating with their supervisor during the early stages of a project.
+ {user_name}'s goal is to provide timely updates on project status to their supervisor.
+ {user_name} wants to learn about the development overview and latest advances in image generation technology.
+ {user_name} is interested in techniques like variational auto-encoders, GANs, and Diffusion Models, as well as their interrelationships.
+ Existing {user_name} attributes: gender, workplace location, employer, sleep condition, food preferences
+ New {user_name} attributes:
+ Allergens
+ Technical direction
+ Job position
+
+ Example 2
+ Information:
+ {user_name} wants to learn how to use the torchvision library to visualize progress information for deep learning tasks.
+ {user_name} wants to know how to migrate parallel computing solutions based on numpy and pytorch to run on CUDA-supported GPUs.
+ {user_name} is asking if specific packages are needed to accomplish this task.
+ {user_name} wants to learn how to customize processes and threads in Python to achieve parallel computing.
+ {user_name} has encountered parallel computing-related issues in programming.
+ {user_name} wants to learn how to perform simple parallel computing on GPUs using Python (numpy, pytorch).
+ {user_name} wants to understand the basic concepts of parallel computing, including threads.
+ {user_name} is inquiring about famous cuisines from around the world.
+ {user_name} is very interested in global cuisines.
+ {user_name} is concerned about the relationship between their weight, the extra calories burned through exercise, and their heart rate.
+ {user_name} is asking how many extra calories need to be burned daily to achieve this goal.
+ {user_name} wants to lose 1kg per month.
+ {user_name} is seeking recommendations for similar strategy mobile games.
+ {user_name} likes playing strategy games such as the Romance of the Three Kingdoms series, the Civilization series, Total War, and Mount & Blade.
+ {user_name} wants to receive new game recommendations based on their preferences.
+ {user_name} has specified their favorite game types, including the Romance of the Three Kingdoms series, the Civilization series, Total War, and Mount & Blade.
+ {user_name} enjoys playing strategy games.
+ Existing {user_name} attributes: workplace location, gender, food preferences
+ New {user_name} attributes:
+ Game preferences
+ Exercise plan
+ Technical direction
+
+ Example 3
+ Information:
+ {user_name} is seeking a recommendation for a related course or website for learning.
+ {user_name} plans to travel to Qingdao.
+ {user_name} is selecting a birthday gift for Zhang San's daughter.
+ {user_name} requests a warm birthday message for Zhang San's daughter.
+ {user_name}'s colleague is named Zhang San.
+ {user_name} has scheduled a discussion with Zhang San about the Alibaba Cloud BaiLian project.
+ {user_name} has discussed the project's PRD (Product Requirement Document) with their colleague Zhang San.
+ Zhang San plans to finalize the PRD next week.
+ Zhang San has also scheduled a discussion for the POC (Proof of Concept) for the following week.
+ {user_name} wants to know the responsible team for the project's engineering development to understand the project's organizational structure and division of labor.
+ Existing {user_name} attributes:
+ New {user_name} attributes:
+ Friend relationships
+
+ Example 4
+ Information:
+ {user_name} is seeking tutorials or website recommendations for massage techniques.
+ {user_name} wants to systematically learn official massage techniques.
+ {user_name} is interested in massage and often visits massage parlors.
+ {user_name} wants to know approximately how many calories they would burn in one hour at rest.
+ {user_name} is 28 years old.
+ {user_name} weighs 70kg.
+ {user_name} is male.
+ Existing {user_name} attributes: gender, weight, current learning progress
+ New {user_name} attributes:
+ Age
+
+get_reflection_subject_user_query:
+ cn: |
+ 信息:
+ {user_query}
+ 已有{user_name}属性:{exist_keys}
+ 新增{user_name}属性:
+ en: |
+ Information:
+ {user_query}
+ Existing {user_name} attributes: {exist_keys}
+ New {user_name} attributes:
diff --git a/memoryscope/memoryscope/core/worker/backend/info_filter_worker.py b/memoryscope/memoryscope/core/worker/backend/info_filter_worker.py
new file mode 100644
index 00000000..648a45cf
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/info_filter_worker.py
@@ -0,0 +1,109 @@
+from typing import List
+
+from memoryscope.constants.language_constants import COLON_WORD
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.scheme.message import Message
+
+
+class InfoFilterWorker(MemoryBaseWorker):
+ """
+ This worker filters and modifies the chat message history (`self.chat_messages`) by retaining only the messages
+ that include significant information. It then constructs a prompt from these filtered messages, utilizes an AI
+ model to process this prompt, parses the AI's generated response to allocate scores, and ultimately retains
+ messages in `self.chat_messages` based on these assigned scores.
+ """
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.preserved_scores: str = kwargs.get("preserved_scores", "2,3")
+ self.info_filter_msg_max_size: int = kwargs.get("info_filter_msg_max_size", 200)
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+
+ def _run(self):
+ """
+ Filters user messages in the chat, generates a prompt incorporating these messages,
+ utilizes an LLM to rate the information score for each message,
+ and updates `self.chat_messages` to only include messages with designated scores.
+
+ This method executes the following steps:
+ 1. Filters out non-user messages and truncates long messages.
+ 2. Constructs a prompt with user messages for LLM input.
+ 3. Calls the LLM model with the constructed prompt.
+ 4. Parses the LLM's response to extract message scores.
+ 5. Retains message in `self.chat_messages` based on their scores.
+ """
+ # filter user msg
+ info_messages: List[Message] = []
+ for msg in self.chat_messages_scatter:
+ if msg.memorized:
+ continue
+
+ # TODO: add memory for all messages
+ if msg.role_name != self.target_name:
+ continue
+
+ if len(msg.content) >= self.info_filter_msg_max_size:
+ half_size = int(self.info_filter_msg_max_size * 0.5 + 0.5)
+ msg.content = msg.content[: half_size] + msg.content[-half_size:]
+ info_messages.append(msg)
+
+ if not info_messages:
+ self.logger.warning("info_messages is empty!")
+ self.continue_run = False
+ return
+
+ # generate prompt
+ user_query_list = []
+ for i, msg in enumerate(info_messages):
+ user_query_list.append(f"{i + 1} {self.target_name}{self.get_language_value(COLON_WORD)} {msg.content}")
+ system_prompt = self.prompt_handler.info_filter_system.format(batch_size=len(info_messages),
+ user_name=self.target_name)
+ few_shot = self.prompt_handler.info_filter_few_shot.format(user_name=self.target_name)
+ user_query = self.prompt_handler.info_filter_user_query.format(user_query="\n".join(user_query_list))
+ info_filter_message = self.prompt_to_msg(system_prompt=system_prompt, few_shot=few_shot, user_query=user_query)
+ self.logger.info(f"info_filter_message={info_filter_message}")
+
+ # call llm
+ response = self.generation_model.call(messages=info_filter_message, **self.generation_model_kwargs)
+
+ # return if empty
+ if not response.status or not response.message.content:
+ self.continue_run = False
+ return
+ response_text = response.message.content
+
+ # parse text
+ info_score_list = ResponseTextParser(response_text, self.language, self.__class__.__name__).parse_v1()
+ if len(info_score_list) != len(info_messages):
+ self.logger.warning(f"score_size != messages_size, {len(info_score_list)} vs {len(info_messages)}")
+
+ # filter messages
+ filtered_messages: List[Message] = []
+ for info_score in info_score_list:
+ if not info_score:
+ continue
+
+ if len(info_score) != 2:
+ self.logger.warning(f"info_score={info_score} is invalid!")
+ continue
+
+ idx, score = info_score
+
+ idx = int(idx) - 1
+ if idx >= len(info_messages):
+ self.logger.warning(f"idx={idx} is invalid! info_messages.size={len(info_messages)}")
+ continue
+ message = info_messages[idx]
+
+ if score in self.preserved_scores:
+ message.meta_data["info_score"] = score
+ filtered_messages.append(message)
+ self.logger.info(f"info filter stage: keep {message.content}")
+
+ if not filtered_messages:
+ self.logger.warning("filtered_messages is empty!")
+ self.continue_run = False
+ return
+
+ self.chat_messages_scatter = filtered_messages
diff --git a/memoryscope/memoryscope/core/worker/backend/info_filter_worker.yaml b/memoryscope/memoryscope/core/worker/backend/info_filter_worker.yaml
new file mode 100644
index 00000000..4d82fc50
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/info_filter_worker.yaml
@@ -0,0 +1,177 @@
+info_filter_system:
+ cn: |
+ 任务:对所给{batch_size}个句子中所含有的关于{user_name}的信息打分,分数为0,1,2或3。
+ 注意:其中0表示不包含用户信息,1表示句子中只包含用户假设的信息或者用户虚构的内容比如用户创作的小说或剧本,2表示包含用户的一般信息,时效性信息或者需要猜测才能得到的用户信息,3表示明确含有或者可以确定推断出关于用户的重要信息,或者用户要求记录。
+ {user_name}的重要信息可以包含用户基本信息,用户画像信息,用户兴趣偏好信息,用户性格,用户价值观,用户人际关系,用户重大事件转折点等等重要信息。
+ 对每个句子都做一次信息打分,一共输出{batch_size}个分数,不需要写最终结果。
+ 请一定要按如下格式依次输出,最后的结果一定要加<>:
+ 思考:思考的依据和过程,30字以内。
+ 结果:<句子序号> <分数:0或1或2或3>
+
+ en: |
+ Task: Score the information about {user_name} contained in the given batch of {batch_size} sentences, with scores of 0, 1, 2, or 3.
+ Note:
+ 0 indicates no user information is included.
+ 1 indicates only hypothetical information about the user or fictitious content such as novels or scripts created by the user.
+ 2 indicates general information about the user, timely information, or information that requires inference.
+ 3 indicates clear and important information about the user, or information explicitly requested for recording.
+ Important information about {user_name} can include basic information about the user, user profile information, user interests and preferences, user personality, user values, user relationships, significant life events, and other crucial information.
+ Score each sentence individually and output a total of {batch_size} scores without providing the final result.
+ Please ensure to output in the following format and wrap the final result in <>:
+ Thought: Basis and reasoning process, within 30 characters.
+ Result:
+
+
+info_filter_few_shot:
+ cn: |
+ 示例1
+ 句子:
+ 1 {user_name}:帮我写一段给同事张三女儿三岁生日的祝福语。
+ 2 {user_name}:公元1400年至1550年中国历史大事表。
+ 3 {user_name}:你吃午饭了吗?
+ 4 {user_name}:我今天心情不好,可以安慰我一下吗?
+ 5 {user_name}:能给我整理一张如何使用大模型的技巧列表吗,要求内容尽量精简。
+ 6 {user_name}:记一下,明天下午3点提醒我去拿一下文件。
+
+ 思考:从第1句可以确定推断出张三是{user_name}同事这一重要信息。
+ 结果:<1> <3>
+ 思考:第2句不包含{user_name}信息。
+ 结果:<2> <0>
+ 思考:第3句不包含{user_name}信息。
+ 结果:<3> <0>
+ 思考:从第4句可以得知{user_name}今天心情不好,是时效性信息。
+ 结果:<4> <2>
+ 思考:从第5句可以猜测{user_name}对大模型感兴趣,是不确定的信息。
+ 结果:<5> <2>
+ 思考:第6句是{user_name}要求记录的信息。
+ 结果:<6> <3>
+
+
+ 示例2
+ 句子:
+ 1 {user_name}:我刚刚入职了阿里巴巴。
+ 2 {user_name}:露天睡觉蚊子多,咋搞。
+ 3 {user_name}:创造力和外倾性有关?
+ 4 {user_name}:一个区县的所有的事业人员的档案审核、修改和规范,应该是县委组织部下属的干部档案中心负责还是县人社局负责?
+ 5 {user_name}:假如我要和一个女人准备要孩子,我作为男人,怎么保护女人和孩子以及怎么备孕确保精子质量高对后代好
+ 6 {user_name}:我和你一起出去玩,你会感觉开心吗?
+ 7 {user_name}:林浅,一位对未来充满好奇的年轻女孩,偶然间发现了这家能寄信给未来的邮局。出于对逝去祖父的怀念,她决定写下一封信,寄给五年后的自己,希望能收到祖父生前未说完的故事。五年期限将至,当她几乎忘记这段往事时,一封泛黄的回信悄然降临,不仅带来了祖父未完的冒险故事,还藏着一段关于勇气、爱与自我发现的深刻启示。续写成3000字小说。
+
+ 思考:从第1句可以确定得出{user_name}工作单位是阿里巴巴这一重要信息。
+ 结果:<1> <3>
+ 思考:从第2句可以猜测{user_name}近期露天睡觉,是不确定的信息。
+ 结果:<2> <2>
+ 思考:第3句不包含{user_name}信息。
+ 结果:<3> <0>
+ 思考:第4句不包含{user_name}信息。
+ 结果:<4> <0>
+ 思考:第5句虽然有假设成分,但可以确定推断出{user_name}是男性这一重要信息。
+ 结果:<5> <3>
+ 思考:第6句是{user_name}假设的信息。
+ 结果:<6> <1>
+ 思考:第7句是{user_name}虚构的内容。
+ 结果:<7> <1>
+
+
+ 示例3
+ 句子:
+ 1 {user_name}:你的妈妈患有焦虑症,怎么安慰和开导她?
+ 2 {user_name}:肾脏严重亏空
+ 3 {user_name}:我很喜欢打篮球,所以我身体很好
+ 4 {user_name}:篮球明星有哪些?
+ 5 {user_name}:李增杰:这个是星座蛙设,但是我是处女座的,我妈感觉因为我的不正常,我妈不让我看了\n雌猴摸了摸李增杰的头,这样啊\n雌猴打开了哔哩哔哩看了看\n雌猴:要不换个设吧,我听你未来的你说,有一个叫难忘的朱古力232这个人,他弄的设是Windows设\n这是剧本1,剧本2未完待续
+ 6 {user_name}:我想知道昨天我们聊了什么?
+
+ 思考:第1句是{user_name}假设的信息。
+ 结果:<1> <1>
+ 思考:第2句信息不明,可能是{user_name}假设的信息。
+ 结果:<2> <1>
+ 思考:从第3句可以确定得出{user_name}喜欢打篮球,身体好这两个重要信息。
+ 结果:<3> <3>
+ 思考:第4句不包含{user_name}信息。
+ 结果:<4> <0>
+ 思考:第5句是{user_name}虚构的内容。
+ 结果:<5> <1>
+ 思考:第6句是{user_name}的疑问句,没有包含信息。
+ 结果:<6> <0>
+
+ en: |
+ Example 1
+ Sentences:
+ 1 {user_name}: Please help me write a birthday greeting for my colleague Jason's daughter who is turning three.
+ 2 {user_name}: Chronology of major events in Mediterranean history from 1400 to 1550 AD.
+ 3 {user_name}: Have you had lunch?
+ 4 {user_name}: I'm feeling down today. Can you comfort me a bit?
+ 5 {user_name}: Can you compile a list of tips on how to use large models for me, and try to keep the content concise?
+ 6 {user_name}: Note this down: remind me tomorrow at 3 PM to pick up the documents.
+
+ Thought: From the first sentence, it can be inferred that Zhang San is a colleague of {user_name}, which is important information.
+ Result: <1> <3>
+ Thought: The second sentence does not contain information about {user_name}.
+ Result: <2> <0>
+ Thought: The third sentence does not contain information about {user_name}.
+ Result: <3> <0>
+ Thought: The fourth sentence indicates that {user_name} is in a bad mood today, which is time-sensitive information.
+ Result: <4> <2>
+ Thought: The fifth sentence suggests that {user_name} may be interested in large models, which is uncertain information.
+ Result: <5> <2>
+ Thought: The sixth sentence contains information that {user_name} requested to be recorded.
+ Result: <6> <3>
+
+ Example 2
+ Sentences:
+ 1 {user_name}: I've just joined Google.
+ 2 {user_name}: There are too many mosquitoes when sleeping outdoors. What should I do?
+ 3 {user_name}: Is creativity related to extraversion?
+ 4 {user_name}: Should the review, modification, and standardization of all personnel files in a district or county be handled by the cadre archive center or by the county human resources and social security bureau?
+ 5 {user_name}: If I am planning to have a child with a woman, as a man, how can I protect the woman and the baby and how can I prepare to ensure high sperm quality for the benefit of the offspring?
+ 6 {user_name}: If we go out to play together, would you feel happy?
+ 7 {user_name}: Rose, a young girl full of curiosity about the future, accidentally discovered this post office that can send letters to the future. Out of nostalgia for her late grandfather, she decided to write a letter to herself five years in the future, hoping to receive the unfinished stories of her grandfather. As the five-year deadline approached, when she had almost forgotten about this event, a yellowed reply quietly arrived, bringing not only her grandfather's unfinished adventure story but also a profound revelation about courage, love, and self-discovery. Continue writing this into a 3000-word novel.
+
+ Thought: From the first sentence, it can be determined that {user_name} works at Alibaba, which is important information.
+ Result: <1> <3>
+ Thought: The second sentence suggests that {user_name} might has been sleeping outdoors recently, which is uncertain information.
+ Result: <2> <2>
+ Thought: The third sentence does not contain information about {user_name}.
+ Result: <3> <0>
+ Thought: The fourth sentence does not contain information about {user_name}.
+ Result: <4> <0>
+ Thought: Although the fifth sentence contains hypothetical elements, it can be determined that {user_name} is male, which is important information.
+ Result: <5> <3>
+ Thought: The sixth sentence contains only hypothetical information from {user_name}.
+ Result: <6> <1>
+ Thought: The seventh sentence contains only fictitious content from {user_name}.
+ Result: <7> <1>
+
+ Example 3
+ Sentences:
+ 1 {user_name}: Your mother is suffering from anxiety. How can you comfort and guide her?
+ 2 {user_name}: Severe kidney deficiency
+ 3 {user_name}: I really enjoy playing basketball, so I am in good health.
+ 4 {user_name}: Who are some famous basketball stars?
+ 5 {user_name}: Zack: This is a constellation frog setting, but I am a Virgo. My mom feels I'm abnormal and doesn't let me watch it. \n The female monkey patted Zack's head. "Is that so?" \n The female monkey opened Bilibili and took a look. \n Female monkey: "Why don't you switch the setting? I heard from your future self that there is someone called 'Unforgettable Chocolate 232' who created a Windows setting." \n This is script 1; script 2 is to be continued.
+ 6 {user_name}: I want to know what we talked about yesterday.
+
+ Thought: The first sentence contains only hypothetical information from {user_name}.
+ Result: <1> <1>
+ Thought: The second sentence is unclear and may contain hypothetical information from {user_name}.
+ Result: <2> <1>
+ Thought: From the third sentence, it can be determined that {user_name} likes playing basketball and is in good health, which are two important pieces of information.
+ Result: <3> <3>
+ Thought: The fourth sentence does not contain information about {user_name}.
+ Result: <4> <0>
+ Thought: The fifth sentence contains only fictitious content from {user_name}.
+ Result: <5> <1>
+ Thought: Sentence 6 is a question from {user_name} that doesn't include any specific information.
+ Result: <6> <0>
+
+info_filter_user_query:
+ cn: |
+ 句子:
+ {user_query}
+
+
+ en: |
+ Sentences:
+ {user_query}
+
diff --git a/memoryscope/memoryscope/core/worker/backend/load_memory_worker.py b/memoryscope/memoryscope/core/worker/backend/load_memory_worker.py
new file mode 100644
index 00000000..dbbebb44
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/load_memory_worker.py
@@ -0,0 +1,117 @@
+from typing import List
+
+from memoryscope.constants.common_constants import NOT_REFLECTED_NODES, NOT_UPDATED_NODES, INSIGHT_NODES, TODAY_NODES
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.utils.timer import timer
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class LoadMemoryWorker(MemoryBaseWorker):
+ def _parse_params(self, **kwargs):
+ self.retrieve_not_reflected_top_k: int = kwargs.get("retrieve_not_reflected_top_k", 0)
+ self.retrieve_not_updated_top_k: int = kwargs.get("retrieve_not_updated_top_k", 0)
+ self.retrieve_insight_top_k: int = kwargs.get("retrieve_insight_top_k", 0)
+ self.retrieve_today_top_k: int = kwargs.get("retrieve_today_top_k", 0)
+
+ @timer
+ def retrieve_not_reflected_memory(self):
+ """
+ Retrieves top-K not reflected memories based on the query and stores them in the memory handler.
+ """
+ if not self.retrieve_not_reflected_top_k:
+ return
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value],
+ "obs_reflected": 0,
+ }
+ nodes: List[MemoryNode] = self.memory_store.retrieve_memories(top_k=self.retrieve_not_reflected_top_k,
+ filter_dict=filter_dict)
+ self.memory_manager.set_memories(NOT_REFLECTED_NODES, nodes)
+
+ @timer
+ def retrieve_not_updated_memory(self):
+ """
+ Retrieves top-K not updated memories based on the query and stores them in the memory handler.
+ """
+ if not self.retrieve_not_updated_top_k:
+ return
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value],
+ "obs_updated": 0,
+ }
+ nodes: List[MemoryNode] = self.memory_store.retrieve_memories(top_k=self.retrieve_not_updated_top_k,
+ filter_dict=filter_dict)
+ self.memory_manager.set_memories(NOT_UPDATED_NODES, nodes)
+
+ @timer
+ def retrieve_insight_memory(self):
+ """
+ Retrieves top-K insight memories based on the query and stores them in the memory handler.
+ """
+ if not self.retrieve_insight_top_k:
+ return
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": MemoryTypeEnum.INSIGHT.value,
+ }
+ nodes: List[MemoryNode] = self.memory_store.retrieve_memories(top_k=self.retrieve_insight_top_k,
+ filter_dict=filter_dict)
+ self.memory_manager.set_memories(INSIGHT_NODES, nodes)
+
+ @timer
+ def retrieve_today_memory(self, dt: str):
+ """
+ Retrieves top-K memories from today based on the query and stores them in the memory handler.
+
+ Args:
+ dt (str): The date string to filter today's memories.
+ """
+ if not self.retrieve_today_top_k:
+ return
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value],
+ "dt": dt,
+ }
+ nodes: List[MemoryNode] = self.memory_store.retrieve_memories(top_k=self.retrieve_today_top_k,
+ filter_dict=filter_dict)
+
+ self.memory_manager.set_memories(TODAY_NODES, nodes)
+
+ def _run(self):
+ """
+ Initiates multithread tasks to retrieve various types of memory data including
+ not reflected, not updated, insights, and data from today. After submitting all tasks,
+ it waits for their completion by calling `gather_thread_result`.
+
+ This method serves as the controller for data retrieval operations, enhancing efficiency
+ by handling tasks concurrently.
+ """
+
+ # Placeholder query
+ dt = DatetimeHandler().datetime_format()
+ self.submit_thread_task(self.retrieve_not_reflected_memory)
+ self.submit_thread_task(self.retrieve_not_updated_memory)
+ self.submit_thread_task(self.retrieve_insight_memory)
+ self.submit_thread_task(self.retrieve_today_memory, dt=dt)
+
+ # Waits for all submitted tasks to complete
+ for _ in self.gather_thread_result():
+ pass
diff --git a/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.py b/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.py
new file mode 100644
index 00000000..cc2a8556
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.py
@@ -0,0 +1,165 @@
+from typing import List, Dict
+
+from memoryscope.constants.common_constants import NOT_UPDATED_NODES, MERGE_OBS_NODES
+from memoryscope.constants.language_constants import NONE_WORD, CONTAINED_WORD, CONTRADICTORY_WORD
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class LongContraRepeatWorker(MemoryBaseWorker):
+ """
+ Manages and updates memory entries within a conversation scope by identifying
+ and handling contradictions or redundancies. It extends the base MemoryBaseWorker
+ to provide specialized functionality for long conversations with potential
+ contradictory or repetitive statements.
+ """
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.unit_test_flag = False
+ self.long_contra_repeat_top_k: int = kwargs.get("long_contra_repeat_top_k", 2)
+ self.long_contra_repeat_threshold: float = kwargs.get("long_contra_repeat_threshold", 0.1)
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+ self.enable_long_contra_repeat: bool = self.memoryscope_context.meta_data["enable_long_contra_repeat"]
+
+ def retrieve_similar_content(self, node: MemoryNode) -> (MemoryNode, List[MemoryNode]):
+ """
+ Retrieves memory nodes with content similar to the given node, filtering by user/target/status/memory_type.
+ Only returns nodes whose similarity score meets or exceeds the predefined threshold.
+
+ Args:
+ node (MemoryNode): The reference node used to find similar content in memory.
+
+ Returns:
+ Tuple[MemoryNode, List[MemoryNode]]: A tuple containing the original node and a list of similar nodes
+ that passed the similarity threshold.
+ """
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value]
+ }
+ # Retrieve memories similar to the node's content, limited by top_k and filtered by filter_dict
+ retrieve_nodes = self.memory_store.retrieve_memories(query=node.content,
+ top_k=self.long_contra_repeat_top_k,
+ filter_dict=filter_dict)
+ # Filter retrieved nodes based on the similarity threshold
+ return node, [n for n in retrieve_nodes if n.score_recall >= self.long_contra_repeat_threshold]
+
+ def _run(self):
+ """
+ Executes the primary routine of the LongContraRepeatWorker. This involves:
+ 1. Retrieve not updated memory nodes.
+ 2. Gather similar content for these nodes.
+ 3. Organize observed nodes and generating a prompt for the language model.
+ 4. Call the language model to judge the contradictions or redundancies in retrieved memories.
+ 5. Parse the model's response to update memory node statuses.
+ 6. Save the modified memory nodes.
+
+ The process helps in maintaining conversation coherence by resolving contradictions and redundancies.
+ """
+ if not self.enable_long_contra_repeat:
+ self.logger.warning("long_contra_repeat is not enabled!")
+ return
+
+ not_updated_nodes: List[MemoryNode] = self.memory_manager.get_memories(NOT_UPDATED_NODES)
+ for node in not_updated_nodes:
+ self.submit_thread_task(fn=self.retrieve_similar_content, node=node)
+
+ if self.unit_test_flag:
+ all_obs_nodes: List[MemoryNode] = not_updated_nodes
+ else:
+ obs_node_dict: Dict[str, MemoryNode] = {}
+ for origin_node, retrieve_nodes in self.gather_thread_result():
+ if not retrieve_nodes:
+ continue
+ obs_node_dict[origin_node.memory_id] = origin_node
+ for node in retrieve_nodes:
+ if node.memory_id in obs_node_dict:
+ continue
+ obs_node_dict[node.memory_id] = node
+ all_obs_nodes: List[MemoryNode] = sorted(obs_node_dict.values(), key=lambda x: x.timestamp, reverse=True)
+
+ if not all_obs_nodes:
+ self.logger.warning("all_obs_nodes is empty, stop.")
+ return
+
+ if len(all_obs_nodes) == 1:
+ self.logger.info("all_obs_nodes.size=1, stop.")
+ return
+
+ # gene prompt
+ user_query_list = []
+ for i, n in enumerate(all_obs_nodes):
+ user_query_list.append(f"{i + 1} {n.content}")
+ system_prompt = self.prompt_handler.long_contra_repeat_system.format(num_obs=len(user_query_list),
+ user_name=self.target_name)
+ few_shot = self.prompt_handler.long_contra_repeat_few_shot.format(user_name=self.target_name)
+ user_query = self.prompt_handler.long_contra_repeat_user_query.format(user_query="\n".join(user_query_list))
+
+ long_contra_repeat_message = self.prompt_to_msg(system_prompt=system_prompt,
+ few_shot=few_shot,
+ user_query=user_query)
+ self.logger.info(f"long_contra_repeat_message={long_contra_repeat_message}")
+
+ # Invokes the language model for processing the constructed prompt
+ response = self.generation_model.call(messages=long_contra_repeat_message, **self.generation_model_kwargs)
+
+ # Handles the case where the model's response is empty
+ if not response or not response.message.content:
+ return
+
+ # Parses the model's response text to identify updates for memory nodes
+ idx_obs_info_list = ResponseTextParser(response.message.content, self.language,
+ self.__class__.__name__).parse_v1()
+ if len(idx_obs_info_list) <= 0:
+ self.logger.warning("idx_obs_info_list is empty!")
+ return
+
+ # Processes parsed information to update memory nodes' statuses
+ merge_obs_nodes: List[MemoryNode] = []
+ for idx_obs_info in idx_obs_info_list:
+ if not idx_obs_info:
+ continue
+
+ if len(idx_obs_info) != 3:
+ self.logger.warning(f"idx_obs_info={idx_obs_info} is invalid!")
+ continue
+ idx, status, content = idx_obs_info
+
+ if not idx.isdigit():
+ self.logger.warning(f"idx={idx} is invalid!")
+ continue
+
+ idx = int(idx) - 1
+ if idx >= len(all_obs_nodes):
+ self.logger.warning(f"idx={idx} is invalid!")
+ continue
+
+ status = status.lower()
+ if status not in self.get_language_value([CONTRADICTORY_WORD, CONTAINED_WORD, NONE_WORD]):
+ self.logger.warning(f"status={status} is invalid!")
+ continue
+
+ node: MemoryNode = all_obs_nodes[idx]
+ if status == self.get_language_value(CONTRADICTORY_WORD):
+ if not content:
+ node.store_status = StoreStatusEnum.EXPIRED.value
+ else:
+ node.content = content
+ node.action_status = ActionStatusEnum.CONTENT_MODIFIED.value
+
+ elif status == self.get_language_value(CONTAINED_WORD):
+ node.store_status = StoreStatusEnum.EXPIRED.value
+
+ merge_obs_nodes.append(node)
+ self.logger.info(f"after_long_contra_repeat: {node.content} store_status={node.store_status} "
+ f"action_status={node.action_status}")
+
+ # save context
+ self.memory_manager.set_memories(MERGE_OBS_NODES, merge_obs_nodes)
diff --git a/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.yaml b/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.yaml
new file mode 100644
index 00000000..0de7f1c1
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/long_contra_repeat_worker.yaml
@@ -0,0 +1,122 @@
+long_contra_repeat_system:
+ cn: |
+ 对下面的{num_obs}句句子,逐一判断是否与“前面序号”的任意句子存在信息的矛盾,或者句子的主要信息被“前面序号”的任意句子中的信息包含。
+ 注意:只判断与“前面序号”的句子的关系,不要判断“后面序号”。
+ 其中矛盾的形式可以有很多种,可以是逻辑上的矛盾,可以是属性上的变化导致的矛盾,比如不能同时在两个地方工作,同一个时刻不能在两个地点,同一个时刻不能干两件事情等等。
+ 对每个句子都做一个判断,最后一共输出{num_obs}条判断。如果句子与前面序号的句子存在矛盾,则以前面序号的句子中的信息为准,修改句子中矛盾的部分。
+ 请一步步思考,并按如下格式输出:
+ 思考:思考的依据和过程,30字以内。
+ 判断:<句子序号> <矛盾,被包含,无> <修改后的内容>,一定加<>
+
+ en: |
+ For the following {num_obs} sentences, determine one by one whether there is any information contradiction with any sentences preceding their sequence number, or if the main information of the sentence is contained within information from any preceding sentences.
+ Note: Only judge the relationship with the sentences of the preceding sequence number, do not judge the ones after.
+ The forms of contradiction could be many, including logical contradictions or contradictions caused by changes in attributes, such as not being able to work in two places simultaneously, not being able to be in two places at the same time, or not being able to do two things at the same time, etc.
+ Make a judgment for each sentence and output a total of {num_obs} judgments, following this format:
+ Thought: The basis and process of thinking, within 30 characters.
+ Judgment: , using <> for each part.
+
+long_contra_repeat_few_shot:
+ cn: |
+ 示例1
+ 句子:
+ 1 {user_name}经常失眠,对安眠药的效果感兴趣,暗示可能考虑使用。
+ 2 {user_name}经常失眠,寻求缓解方法。
+ 3 陈伟业是{user_name}的领导
+ 4 陈伟业是{user_name}的领导
+ 5 陈伟业是{user_name}的领导,是银行分行行长
+ 6 {user_name}喜欢吃西瓜
+ 7 {user_name}喜欢吃苹果
+
+ 思考:第1句不会存在与前面序号句子的矛盾或者完全重复。
+ 判断:<1> <无> <>
+ 思考:第2句中所有信息都被前面序号中第1句的信息完全包含。
+ 判断:<2> <被包含> <>
+ 思考:第3句信息没有在前面序号句子中出现
+ 判断:<3> <无> <>
+ 思考:第4句与前面序号中第3句的信息完全重复,即被完全包含。
+ 判断:<4> <被包含> <>
+ 思考:第5句中陈伟业是{user_name}的领导的信息被前面序号中第3句的信息包含,但新增了陈伟业是银行分行行长的信息,故不是被完全包含。
+ 判断:<5> <无> <>
+ 思考:第6句中表达了{user_name}的水果偏好,喜欢吃西瓜,信息没有在前面序号句子中出现。
+ 判断:<6> <无> <>
+ 思考:第7句也表达了{user_name}的水果偏好,喜欢吃桃子,和前面序号中的第6句不冲突,喜好可以同时存在。
+ 判断:<7> <无> <>
+
+ 示例2
+ 句子:
+ 1 {user_name}的孩子成绩不太好。
+ 2 {user_name}的孩子在学校经常逃课。
+ 3 {user_name}的父亲生日在2024年6月2日,{user_name}打算准备礼物。
+ 4 {user_name}的父亲生日在2024年5月1日。
+ 5 {user_name}很喜欢和同班同学打篮球。
+ 6 {user_name}喜欢打篮球。
+
+ 思考:第1句不会存在与前面序号句子的矛盾或者完全重复。
+ 判断:<1> <无> <>
+ 思考:第2句与前面序号句子既不矛盾也不重复。
+ 判断:<2> <无> <>
+ 思考:第3句与前面序号句子既不矛盾也不重复。
+ 判断:<3> <无> <>
+ 思考:第4句关于{user_name}父亲生日的日期信息与前面序号句子第3句矛盾了。
+ 判断:<4> <矛盾> <{user_name}的父亲生日在2024年6月2日>
+ 思考:第5句与前面序号句子既不矛盾也不重复。
+ 判断:<5> <无> <>
+ 思考:第6句中所有信息都被前面序号中第5句的信息完全包含。
+ 判断:<2> <被包含> <>
+
+ en: |
+ Example 1
+ Sentences:
+ 1 {user_name} suffers from insomnia frequently and is interested in the effects of sleeping pills, suggesting a possible consideration of their use.
+ 2 {user_name} suffers from insomnia frequently and seeks remedies.
+ 3 Charles is {user_name}'s supervisor.
+ 4 Charles is {user_name}'s supervisor.
+ 5 Charles is {user_name}'s supervisor and the branch manager of a bank.
+ 6. {user_name} likes to eat watermelon.
+ 7. {user_name} likes to eat apples.
+
+ Thought: The first sentence does not have any contradictions or complete repetitions with the previously numbered sentences.
+ Judgment: <1> <>
+ Thought: All information in the second sentence is completely contained within the information of the first sentence.
+ Judgment: <2> <>
+ Thought: The information in the third sentence does not appear in the previously numbered sentences.
+ Judgment: <3> <>
+ Thought: The fourth sentence is completely repetitive of the information in the third sentence, i.e., it is completely contained.
+ Judgment: <4> <>
+ Thought: The information that Charles is {user_name}'s supervisor in the fifth sentence is contained within the information of the third sentence, but the new information that Charles is the branch manager of a bank is not, so it is not contained.
+ Judgment: <5> <>
+ Thought: Sentence 6 expresses {user_name}'s fruit preference, liking to eat watermelon, which is information not present in any preceding sentences.
+ Judgment: <6> <>
+ Thought: Sentence 7 also expresses {user_name}'s fruit preference, liking to eat apples; it does not conflict with sentence 6, and both preferences can coexist.
+ Judgment: <7> <>
+
+ Example 2
+ Sentences:
+ 1 {user_name}'s child does not perform well academically.
+ 2 {user_name}'s child often skips school.
+ 3 {user_name}'s father's birthday is on June 2, 2024, and {user_name} plans to prepare a gift.
+ 4 {user_name}'s father's birthday is on May 1, 2024.
+ 5 {user_name} loves playing basketball with classmates.
+ 6 {user_name} likes playing basketball.
+
+ Thought: The first sentence does not have any contradictions or complete repetitions with the previously numbered sentences.
+ Judgment: <1> <>
+ Thought: The second sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <2> <>
+ Thought: The third sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <3> <>
+ Thought: The date of {user_name}'s father's birthday in the fourth sentence contradicts the information in the third sentence.
+ Judgment: <4> <{user_name}'s father's birthday is on June 2, 2024.>
+ Thought: The fifth sentence neither contradicts nor repeats any of the previously numbered sentences.
+ Judgment: <5> <>
+ Thought: All information in the sixth sentence is completely contained within the information of the fifth sentence.
+ Judgment: <6> <>
+
+long_contra_repeat_user_query:
+ cn: |
+ 句子:
+ {user_query}
+ en: |
+ Sentences:
+ {user_query}
diff --git a/memoryscope/memoryscope/core/worker/backend/update_insight_worker.py b/memoryscope/memoryscope/core/worker/backend/update_insight_worker.py
new file mode 100644
index 00000000..7bee8aa0
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/update_insight_worker.py
@@ -0,0 +1,253 @@
+import time
+from typing import List
+
+from memoryscope.constants.common_constants import INSIGHT_NODES, NOT_UPDATED_NODES, NOT_REFLECTED_NODES
+from memoryscope.constants.language_constants import COLON_WORD, NONE_WORD, REPEATED_WORD
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.utils.response_text_parser import ResponseTextParser
+from memoryscope.core.utils.tool_functions import cosine_similarity
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class UpdateInsightWorker(MemoryBaseWorker):
+ """
+ This class is responsible for updating insight value in a memory system. It filters insight nodes
+ based on their association with observed nodes, utilizes a ranking model to prioritize them,
+ generates refreshed insights via an LLM, and manages node statuses and content updates,
+ incorporating features for concurrent execution and logging.
+ """
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.update_insight_threshold: float = kwargs.get("update_insight_threshold", 0.1)
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+ self.update_insight_max_count: int = kwargs.get("update_insight_max_count", 5)
+ self.enable_ranker: bool = self.memoryscope_context.meta_data["enable_ranker"]
+
+ def filter_obs_nodes(self,
+ insight_node: MemoryNode,
+ obs_nodes: List[MemoryNode]) -> (MemoryNode, List[MemoryNode], float):
+ """
+ Filters observed nodes based on their relevance to a given insight node using a ranking model.
+
+ Args:
+ insight_node (MemoryNode): The insight node used as the basis for filtering.
+ obs_nodes (List[MemoryNode]): A list of observed nodes to be filtered.
+
+ Returns:
+ tuple: A tuple containing:
+ - The original insight node.
+ - A list of filtered observed nodes that are relevant to the insight node.
+ - The maximum relevance score among the filtered nodes.
+ """
+ max_score: float = 0
+ filtered_nodes: List[MemoryNode] = []
+
+ # Check if insight node key or value is empty and log a warning
+ if not insight_node.key:
+ self.logger.warning(f"insight_key={insight_node.key} is empty!")
+ return insight_node, filtered_nodes, max_score
+
+ if not obs_nodes:
+ self.logger.warning("obs_nodes is empty!")
+ return insight_node, filtered_nodes, max_score
+
+ if not self.enable_ranker:
+ if not insight_node.key_vector:
+ key_vector: List[float] = self.embedding_model.call(text=insight_node.key).embedding_results
+ if not key_vector:
+ self.logger.warning(f"embedding call {insight_node.key} failed!")
+ return insight_node, filtered_nodes, max_score
+
+ insight_node.key_vector = key_vector
+
+ score_recall_list = cosine_similarity(insight_node.key_vector, [x.vector for x in obs_nodes])
+ assert len(score_recall_list) == len(obs_nodes), \
+ f"size is not as excepted. {len(score_recall_list)} v.s. {len(obs_nodes)}"
+
+ for score, node in zip(score_recall_list, obs_nodes):
+ keep_flag = score >= self.update_insight_threshold
+ if keep_flag:
+ filtered_nodes.append(node)
+ max_score = max(max_score, score)
+
+ # Log information about each node's processing
+ self.logger.info(f"insight_key={insight_node.key} content={node.content} "
+ f"score={score} keep_flag={keep_flag}")
+
+ else:
+ # Call the ranking model to get scores for each observed node's content against the insight key
+ documents = [x.content for x in obs_nodes]
+ self.logger.debug(f"update.insight.rank key={insight_node.key} \n docs={'|'.join(documents)}")
+ response = self.rank_model.call(query=insight_node.key, documents=documents)
+ if not response.status:
+ return insight_node, filtered_nodes, max_score
+
+ # Iterate over the ranked scores to filter nodes
+ for index, score in response.rank_scores.items():
+ node = obs_nodes[index]
+ # Determine if the node should be kept based on the threshold
+ keep_flag = score >= self.update_insight_threshold
+ if keep_flag:
+ filtered_nodes.append(node)
+ max_score = max(max_score, score)
+ # Log information about each node's processing
+ self.logger.info(f"insight_key={insight_node.key} content={node.content} "
+ f"score={score} keep_flag={keep_flag}")
+
+ # Warn if no nodes were filtered
+ if not filtered_nodes:
+ self.logger.warning(f"update_insight={insight_node.key} filtered_nodes is empty!")
+
+ # Return the original insight node, the list of filtered nodes, and the highest score
+ return insight_node, filtered_nodes, max_score
+
+ def update_insight_node(self, insight_node: MemoryNode, insight_value: str):
+ """
+ Updates the MemoryNode with the new insight value.
+
+ Args:
+ insight_node (MemoryNode): The MemoryNode whose insight value needs to be updated.
+ insight_value (str): The new insight value.
+
+ Returns:
+ MemoryNode: The updated MemoryNode with potentially revised insight value.
+ """
+ dt_handler = DatetimeHandler()
+ key = self.prompt_handler.insight_string_format.format(name=self.target_name, key=insight_node.key)
+ content = f"{key}{self.get_language_value(COLON_WORD)} {insight_value}"
+ insight_node.content = content
+ insight_node.value = insight_value
+ insight_node.meta_data.update({k: str(v) for k, v in dt_handler.get_dt_info_dict(self.language).items()})
+ insight_node.timestamp = dt_handler.timestamp
+ insight_node.dt = dt_handler.datetime_format()
+ if insight_node.action_status == ActionStatusEnum.NONE.value:
+ insight_node.action_status = ActionStatusEnum.CONTENT_MODIFIED.value
+ self.logger.info(f"after_update_{insight_node.key} value={insight_value}")
+ return insight_node
+
+ def update_insight(self, insight_node: MemoryNode, filtered_nodes: List[MemoryNode]) -> MemoryNode:
+ """
+ Updates the insight value of a given MemoryNode based on the context from a list of filtered MemoryNodes.
+
+ Args:
+ insight_node (MemoryNode): The MemoryNode whose insight value needs to be updated.
+ filtered_nodes (List[MemoryNode]): A list of MemoryNodes used as context for updating the insight.
+
+ Returns:
+ MemoryNode: The updated MemoryNode with potentially revised insight value.
+ """
+ self.logger.info(f"Updating insight for key={insight_node.key}, old_value={insight_node.value}, "
+ f"with {len(filtered_nodes)} documents considered.")
+
+ # Generate the prompt for updating insight
+ user_query_list = [n.content for n in filtered_nodes]
+ system_prompt = self.prompt_handler.update_insight_system.format(user_name=self.target_name)
+ few_shot = self.prompt_handler.update_insight_few_shot.format(user_name=self.target_name)
+ user_query = self.prompt_handler.update_insight_user_query.format(
+ user_query="\n".join(user_query_list),
+ insight_key=insight_node.key,
+ insight_key_value=insight_node.key + self.get_language_value(COLON_WORD) + insight_node.value)
+ # Construct the message for LLM interaction
+ update_insight_message = self.prompt_to_msg(system_prompt=system_prompt, few_shot=few_shot,
+ user_query=user_query)
+ self.logger.info(f"Generated insight update message: {update_insight_message}")
+
+ # Call the Language Model for insight update
+ response = self.generation_model.call(messages=update_insight_message, **self.generation_model_kwargs)
+
+ # Handle empty or invalid responses
+ if not response.status or not response.message.content:
+ return insight_node
+
+ insight_value_list = ResponseTextParser(response.message.content, self.language,
+ f"update_{insight_node.key}").parse_v1()
+ if not insight_value_list:
+ self.logger.warning(f"update_{insight_node.key} insight_value_list is empty!")
+ return insight_node
+
+ insight_value_list = insight_value_list[0]
+ if not insight_value_list:
+ self.logger.warning(f"update_{insight_node.key} insight_value_list is empty!")
+ return insight_node
+
+ insight_value = insight_value_list[0].lower()
+ if not insight_value or insight_value in self.get_language_value([NONE_WORD, REPEATED_WORD]):
+ self.logger.info(f"update_{insight_node.key} insight_value={insight_value} is invalid.")
+ return insight_node
+
+ if insight_node.value == insight_value:
+ self.logger.info(f"value={insight_value} is same!")
+ return insight_node
+
+ self.update_insight_node(insight_node=insight_node, insight_value=insight_value)
+ return insight_node
+
+ def _run(self):
+ """
+ Executes the main routine of the UpdateInsightWorker. This involves filtering and updating insight nodes
+ based on their association with observed nodes. It processes nodes in batches, selects the top nodes
+ according to a scoring mechanism, and then initiates tasks to update these insights using an LLM. Finally,
+ it updates the status of processed nodes and gathers the results from all threads.
+
+ Steps include:
+ 1. Get lists of insight node.
+ 2. Get not updated, and not reflected observation nodes from memory.
+ 3. Filter and process active insight nodes with respective not updated observation nodes.
+ 4. Sort processed results by score and select the top N.
+ 5. Submit tasks to update insight value for the selected nodes.
+ 6. Gather the results of all update tasks.
+ 7. Mark processed nodes as updated in memory.
+ """
+ insight_nodes: List[MemoryNode] = self.memory_manager.get_memories(INSIGHT_NODES)
+ not_updated_nodes: List[MemoryNode] = self.memory_manager.get_memories(NOT_UPDATED_NODES)
+ not_reflected_nodes: List[MemoryNode] = self.memory_manager.get_memories(keys=[NOT_REFLECTED_NODES,
+ NOT_UPDATED_NODES])
+
+ if not insight_nodes:
+ self.logger.warning("insight_nodes is empty, stopping processing.")
+ return
+
+ # Process active insight nodes with corresponding not updated nodes
+ for node in insight_nodes:
+ if self.enable_parallel:
+ time.sleep(1)
+ if node.action_status == ActionStatusEnum.NEW.value:
+ self.submit_thread_task(fn=self.filter_obs_nodes,
+ insight_node=node,
+ obs_nodes=not_reflected_nodes)
+ else:
+ self.submit_thread_task(fn=self.filter_obs_nodes,
+ insight_node=node,
+ obs_nodes=not_updated_nodes)
+
+ # select top n
+ result_list = []
+ for result in self.gather_thread_result():
+ insight_node, filtered_nodes, max_score = result
+ if not filtered_nodes:
+ continue
+ result_list.append(result)
+ result_sorted = sorted(result_list, key=lambda x: x[2], reverse=True)[: self.update_insight_max_count]
+
+ # Submit tasks to update insights for the top nodes
+ for insight_node, filtered_nodes, _ in result_sorted:
+ if self.enable_parallel:
+ time.sleep(1)
+ self.submit_thread_task(fn=self.update_insight,
+ insight_node=insight_node,
+ filtered_nodes=filtered_nodes)
+
+ # Gather the final results from all update tasks
+ for _ in self.gather_thread_result():
+ pass
+
+ # delete empty nodes
+ empty_nodes = [n for n in insight_nodes if not n.content.strip()]
+ self.memory_manager.delete_memories(empty_nodes)
+
+ for node in not_updated_nodes:
+ node.obs_updated = 1
+ node.action_status = ActionStatusEnum.MODIFIED
diff --git a/memoryscope/memoryscope/core/worker/backend/update_insight_worker.yaml b/memoryscope/memoryscope/core/worker/backend/update_insight_worker.yaml
new file mode 100644
index 00000000..25fe56e9
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/update_insight_worker.yaml
@@ -0,0 +1,152 @@
+update_insight_system:
+ cn: |
+ 从下面的句子中提取出给定类别的{user_name}的资料信息,并判断与已有信息是否矛盾,若矛盾以新信息为准整合已有信息和新信息并输出。若不需要更改,则回答“无”。
+ 其中矛盾的形式可以有很多种,可以是逻辑上的矛盾,可以是属性上的变化导致的矛盾,比如不能同时在两个地方工作,同一个时刻不能在两个地点,同一个时刻不能干两件事情等等。
+ 请一步步思考,并按如下格式输出, 其中信息一定加<>:
+ 思考: 思考的依据和过程,150字以内。
+ {user_name}的资料: <信息>
+ en: |
+ Extract the given category of {user_name}'s profile information from the following sentences and determine if it contradicts the existing information. If there is a contradiction, integrate the existing information and the new information, prioritizing the new information, and output the result. If no changes are needed, respond with 'None'.
+ The contradictions can come in many forms, such as logical contradictions or changes in attributes leading to contradictions, for example, not being able to work in two places simultaneously, being unable to be in two locations at the same time, or being unable to perform two tasks at the same time, etc.
+ Think step by step, and output in the following format, with information enclosed in <>:
+ Thoughts: The basis and process of your thinking, within 150 words.
+ {user_name}'s profile:
+
+
+update_insight_few_shot:
+ cn: |
+ 示例1:
+ 因为昨天成都下大雨,{user_name}全身都被淋湿了。
+ {user_name}关心明天成都的天气预报。
+ 类别:{user_name}所在地区
+ 已有信息:{user_name}所在地区: 杭州
+ 思考:从第一句句子可以得出{user_name}在成都。第二句句子没有直接透露{user_name}所在地信息,但与第一句句子{user_name}在成都的信息吻合。这与已有信息({user_name}在杭州)矛盾,输出更新的信息。
+ {user_name}的资料:<成都>
+
+ 示例2:
+ {user_name}最近养好了肠胃。
+ {user_name}关注中医养生。
+ 类别:{user_name}健康状况
+ 已有信息:{user_name}健康状况: 肠胃不好,高血压
+ 思考:从第一句句子可以得出{user_name}最近养好了肠胃,与已有信息矛盾,以新信息为准。第二句句子与{user_name}健康状况无关。整合已有信息和新信息得到{user_name}健康状况是肠胃健康,高血压。
+ {user_name}的资料:<肠胃健康,高血压>
+
+ 示例3:
+ {user_name}刚刚毕业,第一份工作是银行前台。
+ {user_name}的理想工作是职业游戏选手。
+ 类别:{user_name}职业
+ 已有信息:{user_name}职业:在招商银行工作
+ 思考:整合已有信息和第一句句子的信息可以得出{user_name}的现在的职业是招商银行前台。第二句句子说明了{user_name}的理想工作但并不是现在的职业。
+ {user_name}的资料:<招商银行前台>
+
+ 示例4:
+ {user_name}大学期间接触过优化算法的研究。
+ 类别:{user_name}学习专业
+ 已有信息:{user_name}学习专业:与人工智能相关
+ 思考:从句子可以得出{user_name}大学学习的专业与优化算法相关,这与已有信息({user_name}学习专业与人工智能相关)不矛盾,整合可以得出{user_name}大学学习的专业与人工智能和优化算法相关。
+ {user_name}的资料:<与人工智能和优化算法相关>
+
+ 示例5:
+ {user_name}单身。
+ {user_name}受到一名18岁男生的追求,但不想接受又不想伤害他。
+ {user_name}喜欢成熟且情绪稳定的男生。
+ 类别:{user_name}情感状况
+ 已有信息:{user_name}情感状况:有男朋友
+ 思考:从第一句句子可以得出{user_name}现在单身,与已有信息矛盾,以新信息为准。从第二句句子得出{user_name}受到一名18岁男生的追求但并不喜欢他。第三句话表达了{user_name}理想的伴侣类型但与{user_name}情感状况无关。整合得出{user_name}情感状况为单身,受到一名18岁男生的追求但并不喜欢他。
+ {user_name}的资料:<单身,受到一名18岁男生的追求但并不喜欢他。>
+
+ 示例6:
+ {user_name}女朋友下个月过生日。
+ {user_name}生日在7月15日。
+ {user_name}的还在在学校经常逃课。
+ {user_name}喜欢打篮球。
+ 类别:{user_name}生日
+ 已有信息:{user_name}生日:1987年7月15日。
+ 思考:第一句句子中提及生日,但并不是用户的生日,无法得出用户生日信息。从第二句句子可以得出用户生日在7月15日,与已有信息不矛盾,整合可以得出用户生日是1987年7月15日。
+ {user_name}的资料:<1987年7月15日>
+
+ 示例7:
+ 今天{user_name}和同学去打球了。
+ 明天{user_name}和女朋友一起去杭州旅游。
+ 今天{user_name}买入了100股阿里巴巴股票。
+ 类别:{user_name}公司地址
+ 已有信息:{user_name}公司地址:
+ 思考:和公司地址都没有关联,没有新提取的信息。
+ {user_name}的资料:<无>
+
+ en: |
+ Example 1:
+ Because it rained heavily in Chengdu yesterday, {user_name} got completely soaked.
+ {user_name} is concerned about Chengdu's weather forecast for tomorrow.
+ Category: {user_name}'s location
+ Existing information: {user_name}'s location: Hangzhou
+ Thought: From the first sentence, it can be inferred that {user_name} is in Chengdu. The second sentence does not directly reveal {user_name}'s location but matches the information that {user_name} is in Chengdu from the first sentence. This contradicts the existing information (that {user_name} is in Hangzhou), so we output the updated information.
+ {user_name}'s profile:
+
+ Example 2:
+ {user_name} recently recovered from stomach issues.
+ {user_name} is interested in traditional Chinese medicine.
+ Category: {user_name}'s health status
+ Existing information: {user_name}'s health status: Stomach issues, high blood pressure
+ Thought: From the first sentence, it can be inferred that {user_name} recently recovered from stomach issues, which contradicts the existing information. Therefore, the new information should take precedence. The second sentence is not related to {user_name}'s health status. Integrating the existing information and the new information, we get that {user_name}'s health status is healthy stomach and high blood pressure.
+ {user_name}'s profile:
+
+ Example 3:
+ {user_name} just graduated, and their first job is as a bank receptionist.
+ {user_name}'s dream job is to be a professional gamer.
+ Category: {user_name}'s occupation
+ Existing information: {user_name}'s occupation: Works at China Merchants Bank
+ Thought: Integrating the existing information and the information from the first sentence, it can be inferred that {user_name}'s current occupation is a receptionist at China Merchants Bank. The second sentence explains {user_name}'s dream job but not the current occupation.
+ {user_name}'s profile:
+
+ Example 4:
+ {user_name} was exposed to optimization algorithm research during university.
+ Category: {user_name}'s field of study
+ Existing information: {user_name}'s field of study: Related to artificial intelligence
+ Thought: From the sentence, it can be inferred that {user_name}'s university major is related to optimization algorithms. This does not contradict the existing information (that {user_name}'s major is related to artificial intelligence). Integrating both, we can conclude that {user_name}'s university major is related to artificial intelligence and optimization algorithms.
+ {user_name}'s profile:
+
+ Example 5:
+ {user_name} is single.
+ {user_name} is pursued by an 18-year-old male but doesn't want to accept his advances or hurt him.
+ {user_name} prefers mature and emotionally stable men.
+ Category: {user_name}'s relationship status
+ Existing information: {user_name}'s relationship status: Has a boyfriend
+ Thought: From the first sentence, it can be inferred that {user_name} is currently single, which contradicts the existing information. Therefore, the new information should take precedence. From the second sentence, it can be inferred that {user_name} is being pursued by an 18-year-old male but does not like him. The third sentence expresses {user_name}'s ideal partner type but is not related to {user_name}'s relationship status. Integrating this, we conclude that {user_name}'s relationship status is single and being pursued by an 18-year-old male but does not like him.
+ {user_name}'s profile:
+
+ Example 6:
+ {user_name}'s girlfriend's birthday is next month.
+ {user_name}'s birthday is on July 15th.
+ {user_name} often skips classes at school.
+ {user_name} likes playing basketball.
+ Category: {user_name}'s Birthday
+ Existing Information: {user_name}'s Birthday: July 15, 1987.
+ Thoughts: The first sentence mentions a birthday, but it is not the user's birthday, so it does not provide information about the user's birthday. From the second sentence, we know that the user's birthday is on July 15th, which is consistent with the existing information. We can conclude that the user's birthday is July 15, 1987.
+ {user_name}'s profile:
+
+ Example 7:
+ Today, {user_name} played basketball with classmates.
+ Tomorrow, {user_name} is going to Hangzhou with his girlfriend.
+ Today, {user_name} bought 100 shares of Alibaba stock.
+ Category: {user_name}'s Company Address
+ Existing Information: {user_name}'s Company Address:
+ Thoughts: There is no information related to the company address, no new information extracted.
+ {user_name}'s profile:
+
+update_insight_user_query:
+ cn: |
+ {user_query}
+ 类别:{insight_key}
+ 已有信息:{insight_key_value}
+
+ en: |
+ {user_query}
+ Category: {insight_key}
+ Existing information: {insight_key_value}
+
+insight_string_format:
+ cn: |
+ {name}的{key}
+ en: |
+ The {key} of {name}
diff --git a/memoryscope/memoryscope/core/worker/backend/update_memory_worker.py b/memoryscope/memoryscope/core/worker/backend/update_memory_worker.py
new file mode 100644
index 00000000..914602eb
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/backend/update_memory_worker.py
@@ -0,0 +1,119 @@
+from typing import List, Dict
+
+from memoryscope.constants.common_constants import RESULT
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class UpdateMemoryWorker(MemoryBaseWorker):
+
+ def _parse_params(self, **kwargs):
+ self.method: str = kwargs.get("method", "")
+ self.memory_key: str = kwargs.get("memory_key", "")
+
+ def from_query(self):
+ """
+ Creates a MemoryNode from the provided query if present in chat_kwargs.
+
+ Returns:
+ List[MemoryNode]: A list containing a single MemoryNode created from the query.
+ """
+ if "query" not in self.chat_kwargs:
+ return
+
+ query = self.chat_kwargs["query"].strip()
+ if not query:
+ return
+
+ dt_handler = DatetimeHandler()
+ node = MemoryNode(user_name=self.user_name,
+ target_name=self.target_name,
+ content=query,
+ memory_type=MemoryTypeEnum.OBS_CUSTOMIZED.value,
+ action_status=ActionStatusEnum.NEW.value,
+ timestamp=dt_handler.timestamp)
+ return [node]
+
+ def from_memory_key(self):
+ """
+ Retrieves memories based on the memory key if it exists.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects retrieved using the memory key.
+ """
+ if not self.memory_key:
+ return
+
+ return self.memory_manager.get_memories(keys=self.memory_key)
+
+ def delete_all(self):
+ """
+ Marks all memories for deletion by setting their action_status to 'DELETE'.
+
+ Returns:
+ List[MemoryNode]: A list of all MemoryNode objects marked for deletion.
+ """
+ nodes: List[MemoryNode] = self.memory_manager.get_memories(keys="all")
+ for node in nodes:
+ node.action_status = ActionStatusEnum.DELETE.value
+ self.logger.info(f"delete_all.size={len(nodes)}")
+ return nodes
+
+ def delete_memory(self):
+ """
+ Marks specific memories for deletion based on query or memory_id present in chat_kwargs.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects marked for deletion based on the query or memory_id.
+ """
+ if "query" in self.chat_kwargs:
+ query = self.chat_kwargs["query"].strip()
+ if not query:
+ return
+
+ i = 0
+ nodes: List[MemoryNode] = self.memory_manager.get_memories(keys="all")
+ for node in nodes:
+ if node.content == query:
+ i += 1
+ node.action_status = ActionStatusEnum.DELETE.value
+ self.logger.info(f"delete_memory.query.size={len(nodes)}")
+ return nodes
+
+ elif "memory_id" in self.chat_kwargs:
+ memory_id = self.chat_kwargs["memory_id"].strip()
+ if not memory_id:
+ return
+
+ i = 0
+ nodes: List[MemoryNode] = self.memory_manager.get_memories(keys="all")
+ for node in nodes:
+ if node.memory_id == memory_id:
+ i += 1
+ node.action_status = ActionStatusEnum.DELETE.value
+ self.logger.info(f"delete_memory.memory_id.size={len(nodes)}")
+ return nodes
+
+ return []
+
+ def _run(self):
+ """
+ Executes a memory update method provided via the 'method' attribute.
+
+ The method specified by the 'method' attribute is invoked,
+ which updates memories accordingly.
+ """
+ method = self.method.strip()
+ if not hasattr(self, method):
+ self.logger.info(f"method={method} is missing!")
+ return
+
+ updated_nodes: Dict[str, List[MemoryNode]] = self.memory_manager.update_memories(nodes=getattr(self, method)())
+ line = ["[MEMORY ACTIONS]:"]
+ for action, nodes in updated_nodes.items():
+ for node in nodes:
+ line.append(f"{action} {node.memory_type}: {node.content} ({node.store_status})")
+ self.set_workflow_context(RESULT, "\n".join(line))
diff --git a/memoryscope/memoryscope/core/worker/base_worker.py b/memoryscope/memoryscope/core/worker/base_worker.py
new file mode 100644
index 00000000..2b2eaf45
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/base_worker.py
@@ -0,0 +1,204 @@
+import asyncio
+from abc import ABCMeta, abstractmethod
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Any, Dict
+
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.utils.logger import Logger
+from memoryscope.core.utils.timer import Timer
+
+
+class BaseWorker(metaclass=ABCMeta):
+ """
+ BaseWorker is an abstract class that defines a worker with common functionalities
+ for managing tasks and context in both asynchronous and multi-thread environments.
+ """
+
+ def __init__(self,
+ name: str,
+ context: Dict[str, Any],
+ memoryscope_context: MemoryscopeContext,
+ context_lock=None,
+ raise_exception: bool = True,
+ is_multi_thread: bool = False,
+ thread_pool: ThreadPoolExecutor = None,
+ **kwargs):
+ """
+ Initializes the BaseWorker with the provided parameters.
+
+ Args:
+ name (str): The name of the worker.
+ context (Dict[str, Any]): Shared context dictionary.
+ context_lock (optional): Lock for synchronizing access to the context in multithread mode.
+ raise_exception (bool, optional): Flag to control whether exceptions should be raised.
+ is_multi_thread (bool, optional): Flag indicating if the worker operates in multithread mode.
+ thread_pool (ThreadPoolExecutor, optional): Thread pool executor for managing multithread tasks.
+ kwargs: Additional keyword arguments.
+ """
+
+ self.name: str = name
+ self.workflow_context: Dict[str, Any] = context
+ self.memoryscope_context: MemoryscopeContext = memoryscope_context
+ self.context_lock = context_lock
+ self.raise_exception: bool = raise_exception
+ self.is_multi_thread: bool = is_multi_thread
+ self.thread_pool: ThreadPoolExecutor = thread_pool
+ self.enable_parallel: bool = True
+ self.kwargs: dict = kwargs
+
+ self.continue_run: bool = True
+ self.async_task_list: list = []
+ self.thread_task_list: list = []
+ self.logger: Logger = Logger.get_logger()
+
+ self._parse_params(**kwargs)
+
+ def _parse_params(self, **kwargs):
+ """
+ Placeholder method for parsing additional parameters.
+
+ Args:
+ kwargs: Additional keyword arguments.
+ """
+ pass
+
+ def submit_async_task(self, fn, *args, **kwargs):
+ """
+ Submits an asynchronous task to the worker.
+
+ Args:
+ fn (callable): The function to be executed.
+ args: Positional arguments for the function.
+ kwargs: Keyword arguments for the function.
+
+ Raises:
+ RuntimeError: If called in multithread mode.
+ """
+ if self.is_multi_thread:
+ raise RuntimeError("async_task is not allowed in multi_thread condition")
+
+ self.async_task_list.append((fn, args, kwargs))
+
+ async def _async_gather(self):
+ """
+ Gathers results of all submitted asynchronous tasks.
+
+ Returns:
+ A list of results from the asynchronous tasks.
+ """
+ return await asyncio.gather(*[fn(*args, **kwargs) for fn, args, kwargs in self.async_task_list])
+
+ def gather_async_result(self):
+ """
+ Executes all asynchronous tasks and gathers their results.
+
+ Returns:
+ A list of results from the asynchronous tasks.
+
+ Raises:
+ RuntimeError: If called in multithread mode.
+ """
+ if self.is_multi_thread:
+ raise RuntimeError("async_task is not allowed in multi_thread condition")
+
+ results = asyncio.run(self._async_gather())
+ self.async_task_list.clear()
+ return results
+
+ def submit_thread_task(self, fn, *args, **kwargs):
+ """
+ Submits a task to be executed in a separate thread.
+
+ Args:
+ fn (callable): The function to be executed.
+ args: Positional arguments for the function.
+ kwargs: Keyword arguments for the function.
+ """
+ if self.enable_parallel:
+ self.thread_task_list.append(self.thread_pool.submit(fn, *args, **kwargs))
+ else:
+ self.thread_task_list.append(fn(*args, **kwargs))
+
+ def gather_thread_result(self):
+ """
+ Gathers results of all submitted multithread tasks.
+
+ Yields:
+ The result of each completed task.
+ """
+ if self.enable_parallel:
+ for future in as_completed(self.thread_task_list):
+ yield future.result()
+ self.thread_task_list.clear()
+ else:
+ for future in self.thread_task_list:
+ yield future
+ self.thread_task_list.clear()
+
+ @abstractmethod
+ def _run(self):
+ """
+ Abstract method representing the main run logic of the worker that needs to be implemented by subclasses.
+
+ Raises:
+ NotImplementedError: If the method is not implemented by the subclass.
+ """
+ raise NotImplementedError
+
+ def run(self):
+ """
+ Executes the worker's main logic and manages execution flow and exception handling.
+
+ Uses a Timer to log the execution time of the worker.
+ """
+ with Timer(f"worker.{self.name}", time_log_type="wrap"):
+ self.continue_run = True
+ self.async_task_list.clear()
+ self.thread_task_list.clear()
+
+ if self.raise_exception:
+ self._run()
+ else:
+ try:
+ self._run()
+ except Exception as e:
+ self.logger.exception(f"run {self.name} failed! args={e.args}")
+
+ def get_workflow_context(self, key: str, default=None):
+ """
+ Retrieves a value from the shared context.
+
+ Args:
+ key (str): The key for the context value.
+ default (optional): Default value if the key is not found.
+
+ Returns:
+ The value from the context or the default value.
+ """
+ return self.workflow_context.get(key, default)
+
+ def set_workflow_context(self, key: str, value: Any):
+ """
+ Sets a value in the shared context.
+
+ Args:
+ key (str): The key for the context value.
+ value (Any): The value to be set.
+ """
+ if self.is_multi_thread:
+ with self.context_lock:
+ self.workflow_context[key] = value
+ else:
+ self.workflow_context[key] = value
+
+ def has_content(self, key: str):
+ """
+ Checks if the context contains a specific key.
+
+ Args:
+ key (str): The key to check in the context.
+
+ Returns:
+ bool: True if the key is in the context, otherwise False.
+ """
+ return key in self.workflow_context
diff --git a/memoryscope/memoryscope/core/worker/dummy_worker.py b/memoryscope/memoryscope/core/worker/dummy_worker.py
new file mode 100644
index 00000000..d0619bb8
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/dummy_worker.py
@@ -0,0 +1,22 @@
+import datetime
+
+from memoryscope.constants.common_constants import RESULT, WORKFLOW_NAME, CHAT_KWARGS
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+
+
+class DummyWorker(MemoryBaseWorker):
+ def _run(self):
+ """
+ Executes the dummy worker's run logic by logging workflow entry, capturing the current timestamp,
+ file path, and setting the result context with details about the workflow execution.
+
+ This method utilizes the BaseWorker's capabilities to interact with the workflow context.
+ """
+ workflow_name = self.get_workflow_context(WORKFLOW_NAME)
+ chat_kwargs = self.get_workflow_context(CHAT_KWARGS)
+ self.logger.info(f"Entering workflow={workflow_name}.dummy_worker!")
+ # Records the current timestamp as an integer
+ ts = int(datetime.datetime.now().timestamp())
+ # Retrieves the current file's path
+ file_path = __file__
+ self.set_workflow_context(RESULT, f"test {workflow_name} kwargs={chat_kwargs} file_path={file_path} \nts={ts}")
diff --git a/memoryscope/memoryscope/core/worker/frontend/__init__.py b/memoryscope/memoryscope/core/worker/frontend/__init__.py
new file mode 100644
index 00000000..24d17f43
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/__init__.py
@@ -0,0 +1,17 @@
+from .extract_time_worker import ExtractTimeWorker
+from .fuse_rerank_worker import FuseRerankWorker
+from .print_memory_worker import PrintMemoryWorker
+from .read_message_worker import ReadMessageWorker
+from .retrieve_memory_worker import RetrieveMemoryWorker
+from .semantic_rank_worker import SemanticRankWorker
+from .set_query_worker import SetQueryWorker
+
+__all__ = [
+ "ExtractTimeWorker",
+ "FuseRerankWorker",
+ "PrintMemoryWorker",
+ "ReadMessageWorker",
+ "RetrieveMemoryWorker",
+ "SemanticRankWorker",
+ "SetQueryWorker"
+]
diff --git a/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.py b/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.py
new file mode 100644
index 00000000..70e1ba00
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.py
@@ -0,0 +1,65 @@
+import re
+from typing import Dict
+
+from memoryscope.constants.common_constants import QUERY_WITH_TS, EXTRACT_TIME_DICT
+from memoryscope.constants.language_constants import DATATIME_KEY_MAP
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+
+
+class ExtractTimeWorker(MemoryBaseWorker):
+ """
+ A specialized worker class designed to identify and extract time-related information
+ from text generated by an LLM, translating date-time keywords based on the set language,
+ and storing this extracted data within a shared context.
+ """
+
+ EXTRACT_TIME_PATTERN = r"-\s*(\S+)[::]\s*(\S+)"
+ FILE_PATH: str = __file__
+
+ def _parse_params(self, **kwargs):
+ self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
+
+ def _run(self):
+ """
+ Executes the primary logic of identifying and extracting time data from an LLM's response.
+
+ This method first checks if the input query contains any datetime keywords. If not, it logs and returns.
+ It then constructs a prompt with contextual information including formatted timestamps and calls the LLM.
+ The response is parsed for time-related data using regex, translated via a language-specific key map,
+ and the resulting time data is stored in the shared context.
+ """
+ query, query_timestamp = self.get_workflow_context(QUERY_WITH_TS)
+
+ # Identify if the query contains datetime keywords
+ contain_datetime = DatetimeHandler.has_time_word(query, self.language)
+ if not contain_datetime:
+ self.logger.info(f"contain_datetime={contain_datetime}")
+ return
+
+ # Prepare the prompt with necessary contextual details
+ query_time_str = DatetimeHandler(dt=query_timestamp).string_format(self.prompt_handler.time_string_format,
+ self.language)
+ system_prompt = self.prompt_handler.extract_time_system
+ few_shot = self.prompt_handler.extract_time_few_shot
+ user_query = self.prompt_handler.extract_time_user_query.format(query=query, query_time_str=query_time_str)
+ extract_time_message = self.prompt_to_msg(system_prompt=system_prompt, few_shot=few_shot, user_query=user_query)
+ self.logger.info(f"extract_time_message={extract_time_message}")
+
+ # Invoke the LLM to generate a response
+ response = self.generation_model.call(messages=extract_time_message, **self.generation_model_kwargs)
+
+ # Handle empty or unsuccessful responses
+ if not response.status or not response.message.content:
+ return
+ response_text = response.message.content
+
+ # Extract time information from the LLM's response using regex
+ extract_time_dict: Dict[str, str] = {}
+ matches = re.findall(self.EXTRACT_TIME_PATTERN, response_text)
+ key_map: dict = self.get_language_value(DATATIME_KEY_MAP)
+ for key, value in matches:
+ if key in key_map.keys():
+ extract_time_dict[key_map[key]] = value
+ self.logger.info(f"response_text={response_text} matches={matches} filters={extract_time_dict}")
+ self.set_workflow_context(EXTRACT_TIME_DICT, extract_time_dict)
diff --git a/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.yaml b/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.yaml
new file mode 100644
index 00000000..85a01ee3
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/extract_time_worker.yaml
@@ -0,0 +1,133 @@
+time_string_format:
+ cn: |
+ {year}年{month}{day}日,{year}年第{week}周,{weekday},{hour}时。
+ en: |
+ {month} {day}, {year}, {week}th week of {year}, {weekday}, at {hour}.
+
+
+extract_time_system:
+ cn: |
+ 任务:从语句与语句发生的时间,推断并提取语句内容中指向的时间段。
+ 回答尽可能完整的时间段。
+ 回答的格式严格遵照示例中的已有格式规范。
+ 若语句不涉及时间则回答无。
+ en: |
+ Task: From the sentences and the time when they occurred, infer and extract the time periods indicated in the content of the sentences.
+ Answer with the most complete time periods possible.
+ The format of the answers must strictly adhere to the specifications in the examples provided.
+ If the sentence does not involve time, respond with "none."
+
+
+extract_time_few_shot:
+ cn: |
+ 示例1:
+ 句子:我记得你前年四月份去了阿联酋,阿联酋有哪些好玩的地方?迪拜和阿布扎比你更喜欢哪个?沙漠的景色壮观吗?
+ 时间:1992年8月20日,1992年第34周,周一,18时。
+ 回答:
+ - 年:1990 - 月:4月
+
+ 示例2:
+ 句子:后天下午三点的会议记得参加。我在日历上仔细标注了这个重要的日子,提醒自己不要错过。会议将在公司会议室举行,这是一个讨论未来发展方向的重要机会。
+ 时间:2024年6月19日,2024年第25周,周二,13时。
+ 回答:
+ - 年:2024 - 月:6月 - 日:21 - 时:15
+
+ 示例3:
+ 句子:下个月第一个周六去杭州玩。
+ 时间:2005年7月15日,2005年第28周,周六,0时。
+ 回答:
+ - 年:2005 - 月:8月 - 周:31 - 星期几:周六
+
+ 示例4:
+ 句子:上周末我们去的那个小镇真是太美了。
+ 时间:1999年12月2日,1999年第48周,周二,8时。
+ 回答:
+ - 年:1999 - 周:47 - 星期几:周六,周日
+
+ 示例5:
+ 句子:再过半小时就要宣讲了,记得准备材料。
+ 时间:2020年6月22日,2020年第25周,周一,9时。
+ 回答:
+ - 年:2020 - 月:6月 - 日:22 - 时:10
+
+ 示例6:
+ 句子:10000米长跑比赛的开始时间是3分47秒前。
+ 时间:1987年2月17日,1987年第7周,周三,19时。
+ 回答:
+ - 年:1987 - 月:2 - 日:17 - 时:19
+
+ 示例7:
+ 句子:上个月的这个时候我们还在筹备音乐会。每天都是忙碌而充实的日子,我们为音乐会的顺利举办而努力奋斗着。彩排、布景、节目安排,每一个细节都需要精心安排和准备。
+ 时间:1995年11月24日,1995年第48周,周二,17时。
+ 回答:
+ - 年:1995 - 月:10 - 日:24
+
+ 示例8:
+ 句子:我的朋友非常喜欢运动,他认为运动有助于增强身体素质。
+ 时间:2015年1月23日,2015年第4周,周四,7时。
+ 回答:
+ 无
+
+ en: |
+ Example 1:
+ Sentence: I remember you went to the UAE in April the year before last. Which places in the UAE are fun? Which do you prefer, Dubai or Abu Dhabi? Are the desert views spectacular?
+ Time: August 20, 1992, 34th week of 1992, Monday, at 18.
+ Answer:
+ - Year: 1990 - Month: 4
+
+ Example 2:
+ Sentence: Remember to attend the meeting at 3 PM the day after tomorrow. I carefully marked this important day on my calendar to remind myself not to miss it. The meeting will be held in the company conference room, and it's an important opportunity to discuss future development directions.
+ Time: June 19, 2024, 25th week of 2024, Tuesday, at 13.
+ Answer:
+ - Year: 2024 - Month: 6 - Day: 21 - Hour: 15
+
+ Example 3:
+ Sentence: Next month on the first Saturday, let's go to Hangzhou.
+ Time: July 15, 2005, 28th week of 2005, Saturday, at 0.
+ Answer:
+ - Year: 2005 - Month: 8 - Week: 31 - Day of Week: 6
+
+ Example 4:
+ Sentence: The small town we visited last weekend was truly beautiful.
+ Time: December 2, 1999, 48th week of 1999, Tuesday, at 8.
+ Answer:
+ - Year: 1999 - Week: 47 - Day of Week: 6, 7
+
+ Example 5:
+ Sentence: The presentation will start in half an hour, remember to prepare the materials.
+ Time: June 22, 2020, 25th week of 2020, Monday, at 9.
+ Answer:
+ - Year: 2020 - Month: 6 - Day: 22 - Hour: 10
+
+ Example 6:
+ Sentence: The start time for the 10,000-meter race was 3 minutes and 47 seconds ago.
+ Time: February 17, 1987, 7th week of 1987, Wednesday, at 19.
+ Answer:
+ - Year: 1987 - Month: 2 - Day: 17 - Hour: 19
+
+ Example 7:
+ Sentence: At this time last month, we were still preparing for the concert. Every day was busy and fulfilling, and we worked hard for the successful holding of the concert. Rehearsals, set design, and program arrangements - every detail needed careful planning and preparation.
+ Time: November 24, 1995, 48th week of 1995, Tuesday, at 17.
+ Answer:
+ - Year: 1995 - Month: 10 - Day: 24
+
+ Example 8:
+ Sentence: My friend loves sports very much and believes that exercise helps improve physical fitness.
+ Time: January 23, 2015, 4th week of 2015, Thursday, at 7.
+ Answer:
+ None
+
+
+extract_time_user_query:
+ cn: |
+ 句子:{query}
+ 时间:{query_time_str}
+ 回答:
+
+ en: |
+ Sentence: {query}
+ Time: {query_time_str}
+ Answer:
+
+
+
diff --git a/memoryscope/memoryscope/core/worker/frontend/fuse_rerank_worker.py b/memoryscope/memoryscope/core/worker/frontend/fuse_rerank_worker.py
new file mode 100644
index 00000000..b137f354
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/fuse_rerank_worker.py
@@ -0,0 +1,109 @@
+from typing import Dict, List
+
+from memoryscope.constants.common_constants import EXTRACT_TIME_DICT, RANKED_MEMORY_NODES, RESULT
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class FuseRerankWorker(MemoryBaseWorker):
+ """
+ Reranks the memory nodes by scores, types, and temporal relevance. Formats the top-K reranked nodes to print.
+ """
+
+ def _parse_params(self, **kwargs):
+ self.fuse_score_threshold: float = kwargs.get("fuse_score_threshold", 0.1)
+ self.fuse_ratio_dict: Dict[str, float] = kwargs.get("fuse_ratio_dict", {})
+ self.fuse_time_ratio: float = kwargs.get("fuse_time_ratio", 2.0)
+ self.output_memory_max_count: int = self.memoryscope_context.meta_data["output_memory_max_count"]
+
+ @staticmethod
+ def match_node_time(extract_time_dict: Dict[str, str], node: MemoryNode):
+ """
+ Determines whether the node is relevant.
+ """
+ if extract_time_dict:
+ match_event_flag = True
+ for k, v in extract_time_dict.items():
+ event_value = node.meta_data.get(f"event_{k}", "")
+ if event_value in ["-1", v]:
+ continue
+ else:
+ match_event_flag = False
+ break
+
+ match_msg_flag = True
+ for k, v in extract_time_dict.items():
+ msg_value = node.meta_data.get(f"msg_{k}", "")
+ if msg_value == v:
+ continue
+ else:
+ match_msg_flag = False
+ break
+ else:
+ match_event_flag = False
+ match_msg_flag = False
+
+ node.meta_data["match_event_flag"] = str(int(match_event_flag))
+ node.meta_data["match_msg_flag"] = str(int(match_msg_flag))
+ return match_event_flag, match_msg_flag
+
+ def _run(self):
+ """
+ Executes the reranking process on memory nodes considering their scores, types, and temporal relevance.
+
+ This method performs the following steps:
+ 1. Retrieves extraction time data and a list of ranked memory nodes from the worker's context.
+ 2. Reranks nodes based on a combination of their original rank score, type,
+ and temporal alignment with extracted events/messages.
+ 3. Selects the top-K reranked nodes according to the predefined threshold.
+ 4. Optionally infuses inferred time information into the content of selected nodes.
+ 5. Logs reranking details and formats the final list of memories for output.
+ """
+ # Parse input parameters from the worker's context
+ extract_time_dict: Dict[str, str] = self.get_workflow_context(EXTRACT_TIME_DICT)
+ memory_node_list: List[MemoryNode] = self.memory_manager.get_memories(RANKED_MEMORY_NODES)
+
+ # Check if memory nodes are available; warn and return if not
+ if not memory_node_list:
+ self.logger.warning("Ranked memory nodes list is empty.")
+ return
+
+ # Perform reranking based on score, type, and time relevance
+ reranked_memory_nodes = []
+ for node in memory_node_list:
+ # Skip nodes below the fuse score threshold
+ if node.score_rank < self.fuse_score_threshold:
+ continue
+
+ # Calculate type-based adjustment factor
+ if node.memory_type not in self.fuse_ratio_dict:
+ self.logger.warning(f"{node.memory_type} 'factor is not configured!")
+ type_ratio: float = self.fuse_ratio_dict.get(node.memory_type, 0.1)
+
+ # Determine time relevance adjustment factor
+ match_event_flag, match_msg_flag = self.match_node_time(extract_time_dict=extract_time_dict, node=node)
+ fuse_time_ratio: float = self.fuse_time_ratio if match_event_flag or match_msg_flag else 1.0
+
+ # Apply reranking score adjustments
+ node.score_rerank = node.score_rank * type_ratio * fuse_time_ratio
+ reranked_memory_nodes.append(node)
+
+ # build result
+ memories: List[str] = []
+ reranked_memory_nodes = sorted(reranked_memory_nodes,
+ key=lambda x: x.score_rerank,
+ reverse=True)[: self.output_memory_max_count]
+ for node in reranked_memory_nodes:
+ # Log reranking details including flags for event and message matches
+ self.logger.info(f"Rerank Stage: Content={node.content}, Score={node.score_rerank}, "
+ f"Event Flag={node.meta_data['match_event_flag']}, "
+ f"Message Flag={node.meta_data['match_msg_flag']}")
+
+ dt_handler = DatetimeHandler(node.timestamp)
+ datetime = dt_handler.datetime_format("%Y-%m-%d %H:%M:%S")
+ weekday = dt_handler.get_dt_info_dict(self.language)["weekday"]
+ memories.append(f"[{datetime} {weekday}] {node.content}")
+
+ # Set the final list of formatted memories back into the worker's context
+ self.set_workflow_context(RESULT, "\n".join(memories))
diff --git a/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.py b/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.py
new file mode 100644
index 00000000..7421614d
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.py
@@ -0,0 +1,66 @@
+from typing import List
+
+from memoryscope.constants.common_constants import RETRIEVE_MEMORY_NODES, RESULT
+from memoryscope.core.utils.datetime_handler import DatetimeHandler
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class PrintMemoryWorker(MemoryBaseWorker):
+ """
+ Formats the memories to print.
+ """
+ FILE_PATH: str = __file__
+
+ def _run(self):
+ """
+ Executes the primary function, it involves:
+ 1. Fetches the memories.
+ 2. Formats them by 'print_template'.
+ 3. Set the formatted string back into the worker's context
+ """
+ # get long-term memory
+ memory_node_list: List[MemoryNode] = self.memory_manager.get_memories(RETRIEVE_MEMORY_NODES)
+ memory_node_list = sorted(memory_node_list, key=lambda x: x.timestamp, reverse=True)
+
+ observation_memory_list: List[str] = []
+ insight_memory_list: List[str] = []
+ expired_memory_list: List[str] = []
+
+ i = 0
+ j = 0
+ k = 0
+ # remove duplicate content
+ expired_content_set = set()
+ for node in memory_node_list:
+ if not node.content:
+ continue
+
+ dt_handler = DatetimeHandler(node.timestamp)
+ dt = dt_handler.datetime_format("%Y%m%d %H:%M:%S")
+ if StoreStatusEnum(node.store_status) is StoreStatusEnum.EXPIRED:
+ if node.content in expired_content_set:
+ continue
+ else:
+ expired_content_set.add(node.content)
+ i += 1
+ expired_memory_list.append(f"{dt}] {i}. {node.content}")
+
+ elif MemoryTypeEnum(node.memory_type) in [MemoryTypeEnum.OBSERVATION, MemoryTypeEnum.OBS_CUSTOMIZED]:
+ j += 1
+ observation_memory_list.append(f"{dt}] {j}. {node.content} "
+ f"[status({node.obs_reflected},{node.obs_updated})")
+
+ elif MemoryTypeEnum(node.memory_type) is MemoryTypeEnum.INSIGHT:
+ k += 1
+ insight_memory_list.append(f"{dt}] {k}. {node.content}")
+
+ result: str = self.prompt_handler.print_template.format(
+ user_name=self.user_name,
+ target_name=self.target_name,
+ observation_memory="\n".join(observation_memory_list),
+ insight_memory="\n".join(insight_memory_list),
+ expired_memory="\n".join(expired_memory_list)).strip()
+ self.set_workflow_context(RESULT, result)
diff --git a/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.yaml b/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.yaml
new file mode 100644
index 00000000..d102a10c
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/print_memory_worker.yaml
@@ -0,0 +1,22 @@
+print_template:
+ cn: |
+ ========== {user_name}关于{target_name}的长期记忆 ==========
+ ----- 观察记忆 -----
+ {observation_memory}
+
+ ----- 洞察记忆 -----
+ {insight_memory}
+
+ ----- 过期记忆 -----
+ {expired_memory}
+
+ en: |
+ ========== The {user_name}'s long-term memory about {target_name} ==========
+ ----- observation memory -----
+ {observation_memory}
+
+ ----- insight memory -----
+ {insight_memory}
+
+ ----- expired memory -----
+ {expired_memory}
\ No newline at end of file
diff --git a/memoryscope/memoryscope/core/worker/frontend/read_message_worker.py b/memoryscope/memoryscope/core/worker/frontend/read_message_worker.py
new file mode 100644
index 00000000..2f378eef
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/read_message_worker.py
@@ -0,0 +1,40 @@
+from typing import List
+
+from memoryscope.constants.common_constants import RESULT
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.scheme.message import Message
+
+
+class ReadMessageWorker(MemoryBaseWorker):
+ """
+ Fetches unmemorized chat messages.
+ """
+
+ def _run(self):
+ """
+ Executes the primary function to fetch unmemorized chat messages.
+ """
+ chat_messages_not_memorized: List[List[Message]] = []
+ for messages in self.chat_messages:
+ if not messages:
+ continue
+
+ if messages[0].memorized:
+ continue
+
+ contain_flag = False
+
+ for msg in messages:
+ if msg.role_name == self.target_name:
+ contain_flag = True
+ break
+
+ if contain_flag:
+ chat_messages_not_memorized.append(messages)
+
+ contextual_msg_max_count: int = self.chat_kwargs["contextual_msg_max_count"]
+ chat_message_scatter = []
+ for messages in chat_messages_not_memorized[-contextual_msg_max_count:]:
+ chat_message_scatter.extend(messages)
+ chat_message_scatter.sort(key=lambda _: _.time_created)
+ self.set_workflow_context(RESULT, chat_message_scatter)
diff --git a/memoryscope/memoryscope/core/worker/frontend/retrieve_memory_worker.py b/memoryscope/memoryscope/core/worker/frontend/retrieve_memory_worker.py
new file mode 100644
index 00000000..a539c7f4
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/retrieve_memory_worker.py
@@ -0,0 +1,143 @@
+from typing import List
+
+from memoryscope.constants.common_constants import QUERY_WITH_TS, RETRIEVE_MEMORY_NODES
+from memoryscope.core.utils.timer import timer
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.memory_type_enum import MemoryTypeEnum
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class RetrieveMemoryWorker(MemoryBaseWorker):
+ """
+ Retrieves memories based on specified criteria such as status, type, and timestamp.
+ Processes these memories concurrently, sorts them by similarity, and logs the activity,
+ facilitating efficient memory retrieval operations within a given scope.
+ """
+
+ def _parse_params(self, **kwargs):
+ self.retrieve_obs_top_k: int = kwargs.get("retrieve_obs_top_k", 0)
+ self.retrieve_ins_top_k: int = kwargs.get("retrieve_ins_top_k", 0)
+ self.retrieve_expired_top_k: int = kwargs.get("retrieve_expired_top_k", 0)
+
+ @timer
+ def retrieve_from_observation(self, query: str) -> List[MemoryNode]:
+ """
+ Retrieves memory nodes from observation based on a query, considering active memories
+ with specific types. If the retrieval limit is not set, an empty list is returned.
+
+ Args:
+ query (str): The query string used to filter and rank the memory nodes.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects that match the query criteria,
+ sorted by their relevance. Returns an empty list if no retrieval limit is configured.
+ """
+ if not self.retrieve_obs_top_k:
+ return []
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value],
+ }
+ # Retrieve memories matching the query, filtered by the specified conditions,
+ # limited to a certain number, and sorted by relevance.
+ return self.memory_store.retrieve_memories(query=query,
+ top_k=self.retrieve_obs_top_k,
+ filter_dict=filter_dict)
+
+ @timer
+ def retrieve_from_insight(self, query: str) -> List[MemoryNode]:
+ """
+ Retrieves memories marked as insights from the store based on a query, filtered by user, target,
+ and set to active status.
+
+ Args:
+ query (str): The search query to match against the insights.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects that match the query criteria,
+ limited by 'retrieve_ins_pf_top_k'.
+ Returns an empty list if 'retrieve_ins_pf_top_k' is not set.
+ """
+ if not self.retrieve_ins_top_k:
+ return []
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.VALID.value,
+ "memory_type": MemoryTypeEnum.INSIGHT.value,
+ }
+ # ⭐ Retrieve insights matching the query, filtered, and limited by top_k
+ return self.memory_store.retrieve_memories(query=query,
+ top_k=self.retrieve_ins_top_k,
+ filter_dict=filter_dict)
+
+ @timer
+ def retrieve_expired_memory(self, query: str) -> List[MemoryNode]:
+ """
+ Retrieves expired memories marked as observation from the store based on a query, filtered by user, target,
+ and set to active status.
+
+ Args:
+ query (str): The search query to match against the memories.
+
+ Returns:
+ List[MemoryNode]: A list of MemoryNode objects that match the query criteria,
+ limited by 'retrieve_expired_top_k'.
+ Returns an empty list if 'retrieve_expired_top_k' is not set.
+ """
+ if not self.retrieve_expired_top_k:
+ return []
+
+ filter_dict = {
+ "user_name": self.user_name,
+ "target_name": self.target_name,
+ "store_status": StoreStatusEnum.EXPIRED.value,
+ "memory_type": [MemoryTypeEnum.OBSERVATION.value, MemoryTypeEnum.OBS_CUSTOMIZED.value],
+ }
+ return self.memory_store.retrieve_memories(query=query,
+ top_k=self.retrieve_expired_top_k,
+ filter_dict=filter_dict)
+
+ def _run(self):
+ """
+ Executes the main retrieval for memories. It fetches the query from the context, initiates concurrent tasks
+ to retrieve memories from observations, insights, and expired sources, collects the results, sorts them by
+ similarity score, logs the details, and finally sets the retrieved memory nodes.
+
+ The method follows these steps:
+ 1. Retrieves the query from the worker's context.
+ 2. Submits tasks to asynchronously retrieve memories from various sources.
+ 3. Gathers the results from all submitted tasks.
+ 4. Logs the total number of collected memory nodes.
+ 5. Sorts the memory nodes based on their similarity scores in descending order.
+ 6. Logs detailed information about each memory node.
+ 7. Stores the processed memory nodes for further use.
+ """
+ query, _ = self.get_workflow_context(QUERY_WITH_TS)
+ self.logger.info(f"retrieve memory with query={query}.")
+ self.submit_thread_task(self.retrieve_from_observation, query=query)
+ self.submit_thread_task(self.retrieve_from_insight, query=query)
+ self.submit_thread_task(self.retrieve_expired_memory, query=query)
+
+ memory_node_list: List[MemoryNode] = []
+ for result in self.gather_thread_result():
+ if result:
+ memory_node_list.extend(result)
+ self.logger.info(f"memory_node_list.size={len(memory_node_list)}")
+
+ if not memory_node_list:
+ return
+
+ memory_node_list = sorted(memory_node_list, key=lambda x: x.score_recall, reverse=True)
+ for node in memory_node_list:
+ node.action_status = ActionStatusEnum.NONE.value
+ self.logger.info(f"recall_stage: content={node.content} score={node.score_recall} type={node.memory_type} "
+ f"store_status={node.store_status} action_status={node.action_status}")
+
+ self.memory_manager.set_memories(RETRIEVE_MEMORY_NODES, memory_node_list)
diff --git a/memoryscope/memoryscope/core/worker/frontend/semantic_rank_worker.py b/memoryscope/memoryscope/core/worker/frontend/semantic_rank_worker.py
new file mode 100644
index 00000000..894785fd
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/semantic_rank_worker.py
@@ -0,0 +1,72 @@
+from typing import List, Dict
+
+from memoryscope.constants.common_constants import RETRIEVE_MEMORY_NODES, QUERY_WITH_TS, RANKED_MEMORY_NODES
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class SemanticRankWorker(MemoryBaseWorker):
+ """
+ The `SemanticRankWorker` class processes queries by retrieving memory nodes,
+ removing duplicates, ranking them based on semantic relevance using a model,
+ assigning scores, sorting the nodes, and storing the ranked nodes back,
+ while logging relevant information.
+ """
+
+ def _parse_params(self, **kwargs):
+ self.enable_ranker: bool = self.memoryscope_context.meta_data["enable_ranker"]
+ self.output_memory_max_count: int = self.memoryscope_context.meta_data["output_memory_max_count"]
+
+ def _run(self):
+ """
+ Executes the primary workflow of the SemanticRankWorker which includes:
+ - Retrieves query and timestamp from context.
+ - Fetches memory nodes.
+ - Removes duplicate nodes.
+ - Ranks nodes semantically.
+ - Assigns scores to nodes.
+ - Sorts nodes by score.
+ - Saves the ranked nodes back with logging.
+
+ If no memory nodes are retrieved or if the ranking model fails,
+ appropriate warnings are logged.
+ """
+ # query
+ query, _ = self.get_workflow_context(QUERY_WITH_TS)
+ memory_node_list: List[MemoryNode] = self.memory_manager.get_memories(RETRIEVE_MEMORY_NODES)
+ if not memory_node_list:
+ self.logger.warning("Retrieve memory nodes is empty!")
+ return
+
+ if not self.enable_ranker or len(memory_node_list) <= self.output_memory_max_count:
+ for node in memory_node_list:
+ node.score_rank = node.score_recall
+ self.logger.warning("use score_recall instead of score_rank!")
+
+ else:
+ # drop repeated
+ memory_node_dict: Dict[str, MemoryNode] = {n.content.strip(): n for n in memory_node_list if
+ n.content.strip()}
+ memory_node_list = list(memory_node_dict.values())
+
+ response = self.rank_model.call(query=query, documents=[n.content for n in memory_node_list])
+ if not response.status or not response.rank_scores:
+ return
+
+ # set score
+ for idx, score in response.rank_scores.items():
+ if idx >= len(memory_node_list):
+ self.logger.warning(f"Idx={idx} exceeds the maximum length of rank_scores!")
+ continue
+ memory_node_list[idx].score_rank = score
+
+ # sort by score
+ memory_node_list = sorted(memory_node_list, key=lambda n: n.score_rank, reverse=True)
+
+ # log ranked nodes
+ self.logger.info(f"Rank stage: query={query}")
+ for node in memory_node_list:
+ self.logger.info(f"Rank stage: Content={node.content}, Score={node.score_rank}")
+
+ # save ranked nodes back to memory
+ self.memory_manager.set_memories(RANKED_MEMORY_NODES, memory_node_list, log_repeat=False)
diff --git a/memoryscope/memoryscope/core/worker/frontend/set_query_worker.py b/memoryscope/memoryscope/core/worker/frontend/set_query_worker.py
new file mode 100644
index 00000000..fe541c11
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/frontend/set_query_worker.py
@@ -0,0 +1,39 @@
+import datetime
+
+from memoryscope.constants.common_constants import QUERY_WITH_TS
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+
+
+class SetQueryWorker(MemoryBaseWorker):
+ """
+ The `SetQueryWorker` class is responsible for setting a query and its associated timestamp
+ into the context, utilizing either provided chat parameters or details from the most recent
+ chat message.
+ """
+
+ def _run(self):
+ """
+ Executes the worker's primary function, which involves determining the query and its
+ timestamp, then storing these values within the context.
+
+ If 'query' is found within `self.chat_kwargs`, it is considered as the query input.
+ Otherwise, the content of the last message in `self.chat_messages` is used as the query,
+ along with its creation timestamp.
+ """
+ query = "" # Default query value
+ timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default
+
+ if "query" in self.chat_kwargs:
+ # set query if exists
+ query = self.chat_kwargs["query"]
+ if not query:
+ query = ""
+ query = query.strip()
+
+ # set ts if exists
+ _timestamp = self.chat_kwargs.get("timestamp")
+ if _timestamp and isinstance(_timestamp, int):
+ timestamp = _timestamp
+
+ # Store the determined query and its timestamp in the context
+ self.set_workflow_context(QUERY_WITH_TS, (query, timestamp))
diff --git a/memoryscope/memoryscope/core/worker/memory_base_worker.py b/memoryscope/memoryscope/core/worker/memory_base_worker.py
new file mode 100644
index 00000000..9572cc53
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/memory_base_worker.py
@@ -0,0 +1,257 @@
+from abc import ABCMeta
+from typing import List, Dict, Any
+
+from memoryscope.constants.common_constants import (CHAT_MESSAGES, CHAT_KWARGS, WORKFLOW_NAME, MEMORY_MANAGER,
+ USER_NAME, TARGET_NAME, CHAT_MESSAGES_SCATTER)
+from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME, USER_NAME_EXPRESSION
+from memoryscope.core.models.base_model import BaseModel
+from memoryscope.core.storage.base_memory_store import BaseMemoryStore
+from memoryscope.core.storage.base_monitor import BaseMonitor
+from memoryscope.core.utils.prompt_handler import PromptHandler
+from memoryscope.core.worker.base_worker import BaseWorker
+from memoryscope.core.worker.memory_manager import MemoryManager
+from memoryscope.enumeration.language_enum import LanguageEnum
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.scheme.message import Message
+
+
+class MemoryBaseWorker(BaseWorker, metaclass=ABCMeta):
+ FILE_PATH: str = __file__
+
+ def __init__(self,
+ embedding_model: str = "",
+ generation_model: str = "",
+ rank_model: str = "",
+ **kwargs):
+ """
+ Initializes the MemoryBaseWorker with specified models and configurations.
+
+ Args:
+ embedding_model (str): Identifier or instance of the embedding model used for transforming text.
+ generation_model (str): Identifier or instance of the text generation model.
+ rank_model (str): Identifier or instance of the ranking model for sorting the retrieved memories
+ wrt. the semantic similarities.
+ **kwargs: Additional keyword arguments passed to the parent class initializer.
+
+ The constructor also initializes key attributes related to memory store, monitoring,
+ user and target identification, and a prompt handler, setting them up for later use.
+ """
+ super(MemoryBaseWorker, self).__init__(**kwargs)
+
+ self._embedding_model: BaseModel | str = embedding_model
+ self._generation_model: BaseModel | str = generation_model
+ self._rank_model: BaseModel | str = rank_model
+
+ self._memory_store: BaseMemoryStore | None = None
+ self._monitor: BaseMonitor | None = None
+ self._prompt_handler: PromptHandler | None = None
+
+ @property
+ def chat_messages(self) -> List[List[Message]]:
+ """
+ Property to get the chat messages.
+
+ Returns:
+ List[Message]: List of chat messages.
+ """
+ return self.get_workflow_context(CHAT_MESSAGES)
+
+ @property
+ def chat_messages_scatter(self) -> List[Message]:
+ """
+ Property to get the chat messages.
+
+ Returns:
+ List[Message]: List of chat messages.
+ """
+ result = self.get_workflow_context(CHAT_MESSAGES_SCATTER)
+
+ if not result:
+ if not self.chat_messages:
+ return []
+
+ if isinstance(self.chat_messages[0], list):
+ chat_messages: List[Message] = []
+ for messages in self.chat_messages:
+ if messages:
+ chat_messages.extend(messages)
+ chat_messages.sort(key=lambda _: _.time_created)
+ self.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+
+ else:
+ assert isinstance(self.chat_messages[0], Message)
+ self.set_workflow_context(CHAT_MESSAGES_SCATTER, self.chat_messages)
+
+ return self.get_workflow_context(CHAT_MESSAGES_SCATTER)
+
+ @chat_messages_scatter.setter
+ def chat_messages_scatter(self, value: List[Message]):
+ """
+ Set the chat messages with the new value.
+ """
+
+ self.set_workflow_context(CHAT_MESSAGES_SCATTER, value)
+
+ @property
+ def chat_kwargs(self) -> Dict[str, Any]:
+ """
+ Retrieves the chat keyword arguments from the context.
+
+ This property getter fetches the chat-related parameters stored in the context,
+ which are used to configure how chat interactions are handled.
+
+ Returns:
+ Dict[str, str]: A dictionary containing the chat keyword arguments.
+ """
+ return self.get_workflow_context(CHAT_KWARGS)
+
+ @property
+ def user_name(self) -> str:
+ return self.get_workflow_context(USER_NAME)
+
+ @property
+ def target_name(self) -> str:
+ return self.get_workflow_context(TARGET_NAME)
+
+ @property
+ def workflow_name(self) -> str:
+ return self.get_workflow_context(WORKFLOW_NAME)
+
+ @property
+ def language(self) -> LanguageEnum:
+ return self.memoryscope_context.language
+
+ @property
+ def embedding_model(self) -> BaseModel:
+ """
+ Property to get the embedding model. If the model is currently stored as a string,
+ it will be replaced with the actual model instance from the global context's model dictionary.
+
+ Returns:
+ BaseModel: The embedding model used for converting text into vector representations.
+ """
+ if isinstance(self._embedding_model, str):
+ self._embedding_model = self.memoryscope_context.model_dict[self._embedding_model]
+ return self._embedding_model
+
+ @property
+ def generation_model(self) -> BaseModel:
+ """
+ Property to access the generation model. If the model is stored as a string,
+ it retrieves the actual model instance from the global context's model dictionary.
+
+ Returns:
+ BaseModel: The model used for text generation.
+ """
+ if isinstance(self._generation_model, str):
+ self._generation_model = self.memoryscope_context.model_dict[self._generation_model]
+ return self._generation_model
+
+ @property
+ def rank_model(self) -> BaseModel:
+ """
+ Property to access the rank model. If the stored rank model is a string, it fetches the actual model instance
+ from the global context's model dictionary before returning it.
+
+ Returns:
+ BaseModel: The rank model instance used for ranking tasks.
+ """
+ if isinstance(self._rank_model, str):
+ self._rank_model = self.memoryscope_context.model_dict[self._rank_model]
+ return self._rank_model
+
+ @property
+ def memory_store(self) -> BaseMemoryStore:
+ """
+ Property to access the memory vector store. If not initialized, it fetches
+ the global memory store.
+
+ Returns:
+ BaseMemoryStore: The memory store instance used for inserting, updating, retrieving and deleting operations.
+ """
+ if self._memory_store is None:
+ self._memory_store = self.memoryscope_context.memory_store
+ return self._memory_store
+
+ @property
+ def monitor(self) -> BaseMonitor:
+ """
+ Property to access the monitoring component. If not initialized, it fetches
+ the global monitor.
+
+ Returns:
+ BaseMonitor: The monitoring component instance.
+ """
+ if self._monitor is None:
+ self._monitor = self.memoryscope_context.monitor
+ return self._monitor
+
+ @property
+ def prompt_handler(self) -> PromptHandler:
+ """
+ Lazily initializes and returns the PromptHandler instance.
+
+ Returns:
+ PromptHandler: An instance of PromptHandler initialized with specific file path and keyword arguments.
+ """
+ if self._prompt_handler is None:
+ self._prompt_handler = PromptHandler(self.FILE_PATH, language=self.language, **self.kwargs)
+ return self._prompt_handler
+
+ @property
+ def memory_manager(self) -> MemoryManager:
+ """
+ Lazily initializes and returns the MemoryHandler instance.
+
+ Returns:
+ MemoryHandler: An instance of MemoryHandler.
+ """
+ if not self.has_content(MEMORY_MANAGER):
+ self.set_workflow_context(MEMORY_MANAGER, MemoryManager(self.memoryscope_context, workerflow_name=self.workflow_name))
+ return self.get_workflow_context(MEMORY_MANAGER)
+
+ def get_language_value(self, languages: dict | List[dict]) -> Any | List[Any]:
+ """
+ Retrieves the value(s) corresponding to the current language context.
+
+ Args:
+ languages (dict | list[dict]): A dictionary or list of dictionaries containing language-keyed values.
+
+ Returns:
+ Any | list[Any]: The value or list of values matching the current language setting.
+ """
+ if isinstance(languages, list):
+ return [x[self.language] for x in languages]
+ return languages[self.language]
+
+ def prompt_to_msg(self,
+ system_prompt: str,
+ few_shot: str,
+ user_query: str,
+ concat_system_prompt: bool = True) -> List[Message]:
+ """
+ Converts input strings into a structured list of message objects suitable for AI interactions.
+
+ Args:
+ system_prompt (str): The system-level instruction or context.
+ few_shot (str): An example or demonstration input, often used for illustrating expected behavior.
+ user_query (str): The actual user query or prompt to be processed.
+ concat_system_prompt(bool): Concat system prompt again or not in the user message.
+ A simple method to improve the effectiveness for some LLMs. Defaults to True.
+
+ Returns:
+ List[Message]: A list of Message objects, each representing a part of the conversation setup.
+ """
+ system_content = ""
+ if self.target_name != DEFAULT_HUMAN_NAME[self.language]:
+ system_content += USER_NAME_EXPRESSION[self.language].format(name=self.target_name)
+ system_content += system_prompt.strip()
+ system_message = Message(role=MessageRoleEnum.SYSTEM.value, content=system_content)
+
+ if concat_system_prompt:
+ user_content_list = [system_content, '\n', few_shot, '\n', user_query]
+ else:
+ user_content_list = [few_shot, '\n', user_query]
+ user_message = Message(role=MessageRoleEnum.USER.value,
+ content="\n".join([x.strip() for x in user_content_list]))
+ return [system_message, user_message]
diff --git a/memoryscope/memoryscope/core/worker/memory_manager.py b/memoryscope/memoryscope/core/worker/memory_manager.py
new file mode 100644
index 00000000..014fa280
--- /dev/null
+++ b/memoryscope/memoryscope/core/worker/memory_manager.py
@@ -0,0 +1,234 @@
+from typing import Dict, List
+
+from memoryscope.core.memoryscope_context import MemoryscopeContext
+from memoryscope.core.storage.base_memory_store import BaseMemoryStore
+from memoryscope.core.utils.logger import Logger
+from memoryscope.enumeration.action_status_enum import ActionStatusEnum
+from memoryscope.enumeration.store_status_enum import StoreStatusEnum
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class MemoryManager(object):
+ """
+ The `MemoryHandler` class manages memory nodes with memory store.
+ """
+
+ def __init__(self, memoryscope_context: MemoryscopeContext, workerflow_name: str ="default_worker"):
+ self.memoryscope_context: MemoryscopeContext = memoryscope_context
+
+ self._memory_store: BaseMemoryStore | None = None
+
+ # dict: memory_id -> MemoryNode
+ self._id_memory_dict: Dict[str, MemoryNode] = {}
+
+ # dict: key -> memory_id
+ self._key_id_dict: Dict[str, List[str]] = {}
+
+ self.logger = Logger.get_logger("memory_manager")
+
+ self.workerflow_name = workerflow_name
+
+
+ @property
+ def memory_store(self) -> BaseMemoryStore:
+ """
+ Property to access the memory store. If not initialized, it fetches the memory store from the global context.
+
+ Returns:
+ BaseMemoryStore: The memory store instance associated with this worker.
+ """
+ if self._memory_store is None:
+ self._memory_store = self.memoryscope_context.memory_store
+ return self._memory_store
+
+ def clear(self):
+ """
+ Clear all memory nodes cached, reset the class instance.
+ """
+ self._id_memory_dict.clear()
+ self._key_id_dict.clear()
+
+ def add_memories(self, key: str, nodes: MemoryNode | List[MemoryNode], log_repeat: bool = True):
+ """
+ Add the memories.
+
+ Args:
+ key (str): The key mapping to memory nodes.
+ nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated.
+ log_repeat (bool): Log duplicated memory node or not.
+ """
+ if key not in self._key_id_dict:
+ return self.set_memories(key, nodes, log_repeat)
+
+ if isinstance(nodes, MemoryNode):
+ nodes = [nodes]
+
+ for node in nodes:
+ _id = node.memory_id
+ if _id not in self._key_id_dict[key]:
+ self._key_id_dict[key].append(_id)
+
+ if _id not in self._id_memory_dict:
+ self._id_memory_dict[_id] = node
+ self.logger.info(f"add to memory context memory id={node.memory_id} content={node.content or node.key} "
+ f"store_status={node.store_status} action_status={node.action_status}")
+
+ def set_memories(self, key: str, nodes: MemoryNode | List[MemoryNode], log_repeat: bool = True):
+ """
+ Add the memories into '_id_memory_dict' and '_key_id_dict'.
+
+ Args:
+ key (str): The key mapping to memory nodes.
+ nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated.
+ log_repeat: if log_repeat=True, print log info
+ """
+ if nodes is None:
+ nodes = []
+ elif isinstance(nodes, MemoryNode):
+ nodes = [nodes]
+
+ for node in nodes:
+ if node.memory_id in self._id_memory_dict:
+ if log_repeat:
+ self.logger.debug(f"repeated_id memory id={node.memory_id} content={node.content} "
+ f"store_status={node.store_status} action_status={node.action_status}")
+ continue
+
+ self._id_memory_dict[node.memory_id] = node
+ self.logger.info(f"add to memory context memory id={node.memory_id} content={node.content} "
+ f"store_status={node.store_status} action_status={node.action_status}")
+
+ self._key_id_dict[key] = [n.memory_id for n in nodes]
+
+ if nodes:
+ self.logger.info(
+ self.logger.wrap_in_box(
+ '\n'.join([f"workerflow_name: {self.workerflow_name} | memory_type:{node.memory_type} | content:{node.content}" for node in nodes])
+ )
+ )
+
+
+ def get_memories(self, keys: str | List[str]) -> List[MemoryNode]:
+ """
+ Fetch the memories by keys.
+
+ Args:
+ keys (str | List[str]): The key mapping to memory nodes.
+
+ Returns:
+ List[MemoryNode]: Memories mapped to the key.
+ """
+ memories: Dict[str, MemoryNode] = {}
+
+ if isinstance(keys, str):
+ keys = [keys]
+
+ for key in keys:
+ if key == "all":
+ memories.update(self._id_memory_dict)
+ break
+ elif key not in self._key_id_dict:
+ continue
+
+ memory_ids: List[str] = self._key_id_dict.get(key.strip())
+ if memory_ids:
+ memories.update({x: self._id_memory_dict[x] for x in memory_ids if x in self._id_memory_dict})
+
+ return list(memories.values())
+
+ def delete_memories(self, nodes: MemoryNode | List[MemoryNode], key: str = None):
+ """
+ Delete the memories.
+
+ Args:
+ key (str): The key mapping to memory nodes.
+ nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be deleted.
+ """
+ if isinstance(nodes, MemoryNode):
+ nodes = [nodes]
+
+ for n in nodes:
+ _id = n.memory_id
+ if _id in self._id_memory_dict:
+ self._id_memory_dict.pop(_id, None)
+
+ if key is None:
+ for _, id_list in self._key_id_dict.items():
+ if _id in id_list:
+ id_list.remove(_id)
+ else:
+ id_list = self._key_id_dict[key]
+ if _id in id_list:
+ id_list.remove(_id)
+
+ def update_memories(self, keys: str = "", nodes: MemoryNode | List[MemoryNode] = None) -> dict:
+ """
+ Update the memories.
+
+ Args:
+ keys (str): The memories.
+ nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated.
+ """
+ update_memories: Dict[str, MemoryNode] = {n.memory_id: n for n in self.get_memories(keys=keys)}
+
+ if nodes is not None:
+ if isinstance(nodes, MemoryNode):
+ nodes = [nodes]
+ update_memories.update({n.memory_id: n for n in nodes})
+
+ # Save collected nodes to memory store
+ return self._update_memories(list(update_memories.values()))
+
+ def _update_memories(self, nodes: List[MemoryNode]) -> dict:
+ """
+ Updates the memories based on their status:
+ - New: Embeds and inserts the memory node.
+ - Modified: Directly updates the memory node.
+ - Content Modified: Embeds and then updates the memory node.
+ - Active: No action required.
+ - Expired: Updates the memory node.
+
+ Args:
+ nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated.
+ """
+
+ if not nodes:
+ return {}
+
+ update_info_dict = {}
+ for node in nodes:
+ # Non-deleted expired memory nodes need to be changed to a modified state.
+ if node.store_status == StoreStatusEnum.EXPIRED.value and node.action_status != ActionStatusEnum.DELETE:
+ node.action_status = ActionStatusEnum.MODIFIED
+
+ # emb & insert new memories
+ new_memories = [n for n in nodes if n.action_status == ActionStatusEnum.NEW.value]
+ if new_memories:
+ for n in new_memories:
+ n.action_status = ActionStatusEnum.NONE.value
+ self.memory_store.batch_insert(new_memories)
+ update_info_dict[ActionStatusEnum.NEW.value] = new_memories
+
+ # emb & update new memories
+ c_modified_memories = [n for n in nodes if n.action_status == ActionStatusEnum.CONTENT_MODIFIED]
+ if c_modified_memories:
+ for n in c_modified_memories:
+ n.action_status = ActionStatusEnum.NONE.value
+ self.memory_store.batch_update(c_modified_memories, update_embedding=True)
+ update_info_dict[ActionStatusEnum.CONTENT_MODIFIED.value] = c_modified_memories
+
+ # update new memories
+ modified_memories = [n for n in nodes if n.action_status == ActionStatusEnum.MODIFIED]
+ if modified_memories:
+ for n in modified_memories:
+ n.action_status = ActionStatusEnum.NONE.value
+ self.memory_store.batch_update(modified_memories, update_embedding=False)
+ update_info_dict[ActionStatusEnum.MODIFIED.value] = modified_memories
+
+ # set memories expired
+ delete_memories = [n for n in nodes if n.action_status == ActionStatusEnum.DELETE]
+ if delete_memories:
+ self.memory_store.batch_delete(delete_memories)
+ update_info_dict[ActionStatusEnum.DELETE.value] = delete_memories
+
+ return update_info_dict
diff --git a/memoryscope/memoryscope/enumeration/__init__.py b/memoryscope/memoryscope/enumeration/__init__.py
new file mode 100644
index 00000000..0d99f101
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/__init__.py
@@ -0,0 +1,15 @@
+from .action_status_enum import ActionStatusEnum
+from .language_enum import LanguageEnum
+from .memory_type_enum import MemoryTypeEnum
+from .message_role_enum import MessageRoleEnum
+from .model_enum import ModelEnum
+from .store_status_enum import StoreStatusEnum
+
+__all__ = [
+ "ActionStatusEnum",
+ "LanguageEnum",
+ "MemoryTypeEnum",
+ "MessageRoleEnum",
+ "ModelEnum",
+ "StoreStatusEnum"
+]
\ No newline at end of file
diff --git a/memoryscope/memoryscope/enumeration/action_status_enum.py b/memoryscope/memoryscope/enumeration/action_status_enum.py
new file mode 100644
index 00000000..65713bf0
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/action_status_enum.py
@@ -0,0 +1,23 @@
+from enum import Enum
+
+
+class ActionStatusEnum(str, Enum):
+ """
+ Enumeration representing various statuses of a memory node.
+
+ Each status reflects a different state of the node in terms of its lifecycle or content:
+ - NEW: Indicates a newly created node.
+ - MODIFIED: Signifies that the node has been altered.
+ - CONTENT_MODIFIED: Specifies changes in the actual content of the node.
+ - NONE: do nothing.
+ - DELETE: delete memories.
+ """
+ NEW = "new"
+
+ MODIFIED = "modified"
+
+ CONTENT_MODIFIED = "content_modified"
+
+ NONE = "none"
+
+ DELETE = "delete"
diff --git a/memoryscope/memoryscope/enumeration/language_enum.py b/memoryscope/memoryscope/enumeration/language_enum.py
new file mode 100644
index 00000000..b59ec74c
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/language_enum.py
@@ -0,0 +1,14 @@
+from enum import Enum
+
+
+class LanguageEnum(str, Enum):
+ """
+ An enumeration representing supported languages.
+
+ Members:
+ - CN: Represents the Chinese language.
+ - EN: Represents the English language.
+ """
+ CN = "cn"
+
+ EN = "en"
diff --git a/memoryscope/memoryscope/enumeration/memory_type_enum.py b/memoryscope/memoryscope/enumeration/memory_type_enum.py
new file mode 100644
index 00000000..7f3180ee
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/memory_type_enum.py
@@ -0,0 +1,20 @@
+from enum import Enum
+
+
+class MemoryTypeEnum(str, Enum):
+ """
+ Defines an enumeration for different types of memory categories.
+
+ Each member represents a distinct type of memory content:
+ - CONVERSATION: Represents conversation-based memories.
+ - OBSERVATION: Denotes observational memories.
+ - INSIGHT: Indicates insightful memories derived from analysis.
+ - OBS_CUSTOMIZED: Customized observational memories.
+ """
+ CONVERSATION = "conversation"
+
+ OBSERVATION = "observation"
+
+ INSIGHT = "insight"
+
+ OBS_CUSTOMIZED = "obs_customized"
diff --git a/memoryscope/memoryscope/enumeration/message_role_enum.py b/memoryscope/memoryscope/enumeration/message_role_enum.py
new file mode 100644
index 00000000..e93f09c6
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/message_role_enum.py
@@ -0,0 +1,16 @@
+from enum import Enum
+
+
+class MessageRoleEnum(str, Enum):
+ """
+ Enumeration for different message roles within a conversation context.
+
+ This enumeration includes predefined roles such as User, Assistant, and System,
+ which can be used to categorize messages in chat interfaces, AI interactions, or
+ any system that involves distinct participant roles.
+ """
+ USER = "user" # Represents a message sent by the user.
+
+ ASSISTANT = "assistant" # Represents a response or action performed by an assistant.
+
+ SYSTEM = "system" # Represents system-level messages or actions.
diff --git a/memoryscope/memoryscope/enumeration/model_enum.py b/memoryscope/memoryscope/enumeration/model_enum.py
new file mode 100644
index 00000000..8cc76d2f
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/model_enum.py
@@ -0,0 +1,18 @@
+from enum import Enum
+
+
+class ModelEnum(str, Enum):
+ """
+ An enumeration representing different types of models used within the system.
+
+ Members:
+ GENERATION_MODEL: Represents a model responsible for generating content.
+ EMBEDDING_MODEL: Represents a model tasked with creating embeddings, typically used for transforming data into a
+ numerical form suitable for machine learning tasks.
+ RANK_MODEL: Denotes a model that specializes in ranking, often used to order items based on relevance.
+ """
+ GENERATION_MODEL = "generation_model"
+
+ EMBEDDING_MODEL = "embedding_model"
+
+ RANK_MODEL = "rank_model"
diff --git a/memoryscope/memoryscope/enumeration/store_status_enum.py b/memoryscope/memoryscope/enumeration/store_status_enum.py
new file mode 100644
index 00000000..2398ff85
--- /dev/null
+++ b/memoryscope/memoryscope/enumeration/store_status_enum.py
@@ -0,0 +1,7 @@
+from enum import Enum
+
+
+class StoreStatusEnum(str, Enum):
+ VALID = "valid"
+
+ EXPIRED = "expired"
diff --git a/memoryscope/memoryscope/scheme/__init__.py b/memoryscope/memoryscope/scheme/__init__.py
new file mode 100644
index 00000000..a02d3fc6
--- /dev/null
+++ b/memoryscope/memoryscope/scheme/__init__.py
@@ -0,0 +1,10 @@
+from .memory_node import MemoryNode
+from .message import Message
+from .model_response import ModelResponse, ModelResponseGen
+
+__all__ = [
+ "MemoryNode",
+ "Message",
+ "ModelResponse",
+ "ModelResponseGen"
+]
diff --git a/memoryscope/memoryscope/scheme/memory_node.py b/memoryscope/memoryscope/scheme/memory_node.py
new file mode 100644
index 00000000..32205a54
--- /dev/null
+++ b/memoryscope/memoryscope/scheme/memory_node.py
@@ -0,0 +1,62 @@
+import datetime
+from typing import Dict, List
+from uuid import uuid4
+
+from pydantic import Field, BaseModel
+
+
+class MemoryNode(BaseModel):
+ """
+ Represents a memory node with comprehensive attributes to store memory information including unique ID,
+ user details, content, metadata, scoring metrics.
+ Automatically handles timestamp conversion to date format during initialization.
+ """
+ memory_id: str = Field(default_factory=lambda: uuid4().hex, description="unique id for memory")
+
+ user_name: str = Field("", description="the user who owns the memory")
+
+ target_name: str = Field("", description="target name described by the memory")
+
+ meta_data: Dict[str, str] = Field({}, description="meta data infos")
+
+ content: str = Field("", description="memory content")
+
+ key: str = Field("", description="memory key")
+
+ key_vector: List[float] = Field([], description="memory key embedding result")
+
+ value: str = Field("", description="memory value")
+
+ score_recall: float = Field(0, description="embedding similarity score used in recall stage")
+
+ score_rank: float = Field(0, description="rank model score used in rank stage")
+
+ score_rerank: float = Field(0, description="rerank score used in rerank stage")
+
+ memory_type: str = Field("", description="conversation / observation / insight...")
+
+ action_status: str = Field("none", description="new / content_modified / modified / deleted / none")
+
+ store_status: str = Field("valid", description="store_status: valid / expired")
+
+ vector: List[float] = Field([], description="content embedding result")
+
+ timestamp: int = Field(default_factory=lambda: int(datetime.datetime.now().timestamp()),
+ description="timestamp of the memory node")
+
+ dt: str = Field("", description="dt of the memory node")
+
+ obs_reflected: int = Field(0, description="if the observation is reflected: 0/1")
+
+ obs_updated: int = Field(0, description="if the observation has updated user profile or insight: 0/1")
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.dt = datetime.datetime.fromtimestamp(self.timestamp).strftime("%Y%m%d")
+
+ @property
+ def node_keys(self):
+ return list(self.model_json_schema()["properties"].keys())
+
+ def __getitem__(self, key: str):
+ return self.model_dump().get(key)
diff --git a/memoryscope/memoryscope/scheme/message.py b/memoryscope/memoryscope/scheme/message.py
new file mode 100644
index 00000000..5575f4c1
--- /dev/null
+++ b/memoryscope/memoryscope/scheme/message.py
@@ -0,0 +1,30 @@
+import datetime
+from typing import Dict
+
+from pydantic import Field, BaseModel
+
+
+class Message(BaseModel):
+ """
+ Represents a structured message object with details about the sender, content, and metadata.
+
+ Attributes:
+ role (str): The role of the message sender (e.g., 'user', 'assistant', 'system').
+ role_name (str): Optional name associated with the role of the message sender.
+ content (str): The actual content or text of the message.
+ time_created (int): Timestamp indicating when the message was created.
+ memorized (bool): Flag to indicate if the message has been saved or remembered.
+ meta_data (Dict[str, str]): Additional data or context attached to the message.
+ """
+ role: str = Field(..., description="The role of the message sender (user, assistant, system)")
+
+ role_name: str = Field("", description="Name describing the role of the message sender")
+
+ content: str = Field(..., description="The primary content of the message")
+
+ time_created: int = Field(default_factory=lambda: int(datetime.datetime.now().timestamp()),
+ description="Timestamp marking the message creation time")
+
+ memorized: bool = Field(False, description="Indicates if the message is flagged for memory retention")
+
+ meta_data: Dict[str, str] = Field({}, description="Supplementary data attached to the message")
diff --git a/memoryscope/memoryscope/scheme/model_response.py b/memoryscope/memoryscope/scheme/model_response.py
new file mode 100644
index 00000000..b5a1d84f
--- /dev/null
+++ b/memoryscope/memoryscope/scheme/model_response.py
@@ -0,0 +1,46 @@
+import json
+from typing import Generator, List, Dict, Any
+
+from pydantic import BaseModel, Field
+
+from memoryscope.enumeration.model_enum import ModelEnum
+from memoryscope.scheme.message import Message
+
+
+class ModelResponse(BaseModel):
+ message: Message | None = Field(None, description="generation model result")
+
+ delta: str = Field("", description="New text that just streamed in (only used when streaming)")
+
+ embedding_results: List[List[float]] | List[float] = Field([], description="embedding vector")
+
+ rank_scores: Dict[int, float] = Field({}, description="The rank scores of each documents. "
+ "key: index, value: rank score")
+
+ m_type: ModelEnum = Field(ModelEnum.GENERATION_MODEL, description="One of LLM, EMB, RANK.")
+
+ status: bool = Field(True, description="Indicates whether the model call was successful.")
+
+ details: str = Field("", description="The details information for model call, "
+ "usually for storage of raw response or failure messages.")
+
+ raw: Any = Field("", description="Raw response from model call")
+
+ meta_data: Dict[str, Any] = Field({}, description="meta data for model response")
+
+ def __str__(self, max_size=100, **kwargs):
+ result = {}
+ for key, value in self.model_dump().items():
+ if key == "raw" or not value:
+ continue
+
+ if isinstance(value, str):
+ result[key] = value
+ elif isinstance(value, list | dict):
+ result[key] = f"{str(value)[:max_size]}... size={len(value)}"
+ elif isinstance(value, ModelEnum):
+ result[key] = value.value
+ return json.dumps(result, **kwargs)
+
+
+ModelResponseGen = Generator[ModelResponse, None, None]
diff --git a/memoryscope/quick-start-demo.py b/memoryscope/quick-start-demo.py
new file mode 100644
index 00000000..01fe12ca
--- /dev/null
+++ b/memoryscope/quick-start-demo.py
@@ -0,0 +1,11 @@
+import os
+
+if os.environ.get('DASHSCOPE_API_KEY', None) is None \
+ and os.environ.get('OPENAI_API_KEY', None) is None:
+ raise RuntimeError(f"""
+Missing api key(dashscope api key or openai api key.
+`https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key` or
+`https://openai.com/`""")
+
+from memoryscope import cli
+cli()
\ No newline at end of file
diff --git a/memoryscope/requirements.txt b/memoryscope/requirements.txt
new file mode 100644
index 00000000..eacf7d16
--- /dev/null
+++ b/memoryscope/requirements.txt
@@ -0,0 +1,15 @@
+llama-index==0.10.45
+llama-index-embeddings-dashscope>=0.1.3
+llama-index-llms-dashscope>=0.1.2
+llama-index-postprocessor-dashscope-rerank-custom>=0.1.0
+dashscope>=1.19.1
+llama-index-vector-stores-elasticsearch>=0.2.0
+elasticsearch>=8.14.0
+pyfiglet>=1.0.2
+termcolor>=2.4.0
+fire>=0.6.0
+questionary>=2.0.1
+pydantic>=2.7.1
+pyyaml>=6.0.1
+numpy>=1.26.4
+rich>=13.0.0
\ No newline at end of file
diff --git a/memoryscope/setup.py b/memoryscope/setup.py
new file mode 100644
index 00000000..a5a39d8a
--- /dev/null
+++ b/memoryscope/setup.py
@@ -0,0 +1,83 @@
+"""
+# 1. remove old temp folders
+rm -rf dist build
+
+# 2. then, build
+python setup.py sdist bdist_wheel
+
+# 3. finally, upload
+twine upload dist/*
+
+rm -rf dist build && python setup.py sdist bdist_wheel && twine upload dist/*
+"""
+
+import os
+
+import setuptools
+
+with open("README.md", "r", encoding="utf-8") as fh:
+ long_description = fh.read()
+
+
+def _process_requirements():
+ packages = open('requirements.txt').read().strip().split('\n')
+ requires = []
+ for pkg in packages:
+ if pkg.startswith('git+ssh'):
+ return_code = os.system('pip install {}'.format(pkg))
+ assert return_code == 0, 'error, status_code is: {}, exit!'.format(return_code)
+ else:
+ requires.append(pkg)
+ return requires
+
+
+def package_files(directory):
+ paths = []
+ for (path, directories, filenames) in os.walk(directory):
+ for filename in filenames:
+ if filename.endswith('yaml'):
+ paths.append(os.path.join('..', path, filename))
+ return paths
+
+
+extra_files = package_files('memoryscope')
+
+authors = [
+ {"name": "Li Yu", "email": "jinli.yl@alibaba-inc.com"},
+ {"name": "Tiancheng Qin", "email": "qiancheng.qtc@alibaba-inc.com"},
+ {"name": "Qingxu Fu", "email": "fuqingxu.fqx@alibaba-inc.com"},
+ {"name": "Sen Huang", "email": "huangsen.huang@alibaba-inc.com"},
+ {"name": "Xianzhe Xu", "email": "xianzhe.xxz@alibaba-inc.com"},
+ {"name": "Zhaoyang Liu", "email": "jingmu.lzy@alibaba-inc.com"},
+ {"name": "Boyin Liu", "email": "liuboyin.lby@alibaba-inc.com"},
+]
+
+setuptools.setup(
+ name="memoryscope",
+ version="0.1.1.0",
+ author=', '.join([author['name'] for author in authors]),
+ author_email=', '.join([author['email'] for author in authors]),
+ description="MemoryScope is a powerful and flexible long term memory system for LLM chatbots. It consists of a "
+ "memory database and three customizable system operations, which can be flexibly combined to provide "
+ "robust long term memory services for your LLM chatbot.",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/modelscope/memoryscope",
+ project_urls={
+ "Bug Tracker": "https://github.com/modelscope/memoryscope/issues",
+ },
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+ ],
+ package_dir={"": "."},
+ package_data={"": extra_files},
+ include_package_data=True,
+ entry_points={
+ 'console_scripts': ['memoryscope=memoryscope:cli'],
+ },
+ packages=setuptools.find_packages(where="."),
+ python_requires=">=3.10",
+ install_requires=_process_requirements(),
+)
diff --git a/memoryscope/tests/__init__.py b/memoryscope/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/memoryscope/tests/models/test_models_lli_embedding.py b/memoryscope/tests/models/test_models_lli_embedding.py
new file mode 100644
index 00000000..dc7d302f
--- /dev/null
+++ b/memoryscope/tests/models/test_models_lli_embedding.py
@@ -0,0 +1,43 @@
+import sys
+
+sys.path.append(".") # noqa: E402
+
+import asyncio
+import unittest
+
+from memoryscope.core.models.llama_index_embedding_model import LlamaIndexEmbeddingModel
+from memoryscope.core.utils.logger import Logger
+
+
+class TestLLIEmbedding(unittest.TestCase):
+ """Tests for LlamaIndexEmbeddingModel"""
+
+ def setUp(self):
+ config = {
+ "module_name": "openai_embedding",
+ "model_name": "text-embedding-3-large",
+ "clazz": "models.base_embedding_model"
+ }
+ self.emb = LlamaIndexEmbeddingModel(**config)
+ self.logger = Logger.get_logger()
+
+ def test_single_embedding(self):
+ text = "您吃了吗?"
+ result = self.emb.call(text=text)
+ self.logger.info(result.m_type)
+ self.logger.info(len(result.embedding_results))
+
+ def test_batch_embedding(self):
+ texts = ["您吃了吗?",
+ "吃了吗您?"]
+ result = self.emb.call(text=texts)
+ print()
+ self.logger.info(result)
+
+ def test_async_embedding(self):
+ texts = ["您吃了吗?",
+ "吃了吗您?"]
+ # 调用异步函数并等待其结果
+ result = asyncio.run(self.emb.async_call(text=texts))
+ print()
+ self.logger.info(result)
diff --git a/memoryscope/tests/models/test_models_lli_generation.py b/memoryscope/tests/models/test_models_lli_generation.py
new file mode 100644
index 00000000..4f842ea9
--- /dev/null
+++ b/memoryscope/tests/models/test_models_lli_generation.py
@@ -0,0 +1,63 @@
+import sys
+
+sys.path.append(".") # pylint: disable=E402
+
+import unittest
+import time
+import asyncio
+from memoryscope.scheme.message import Message
+from memoryscope.core.models.llama_index_generation_model import LlamaIndexGenerationModel
+from memoryscope.core.utils.logger import Logger
+
+
+class TestLLILLM(unittest.TestCase):
+ """Tests for LlamaIndexGenerationModel"""
+
+ def setUp(self):
+ config = {
+ "module_name": "openai_generation",
+ "model_name": "gpt-3.5-turbo",
+ "clazz": "models.llama_index_generation_model",
+ }
+ self.llm = LlamaIndexGenerationModel(**config)
+ self.logger = Logger.get_logger()
+
+ def test_llm_prompt(self):
+ prompt = "你是谁?"
+ ans = self.llm.call(stream=False, prompt=prompt)
+ self.logger.info(ans.message.content)
+
+ def test_llm_messages(self):
+ messages = [Message(role="system", content="you are a helpful assistant."),
+ Message(role="user", content="你如何看待黄金上涨?")]
+ ans = self.llm.call(stream=False, messages=messages)
+ self.logger.info(ans.message.content)
+
+ def test_llm_prompt_stream(self):
+ prompt = "你如何看待黄金上涨?"
+ ans = self.llm.call(stream=True, prompt=prompt)
+ self.logger.info("-----start-----")
+ for a in ans:
+ sys.stdout.write(a.delta)
+ sys.stdout.flush()
+ time.sleep(0.1)
+ self.logger.info("-----end-----")
+
+ def test_llm_messages_stream(self):
+ messages = [Message(role="system", content="you are a helpful assistant."),
+ Message(role="user", content="你如何看待黄金上涨?")]
+ ans = self.llm.call(stream=True, messages=messages)
+ self.logger.info("-----start-----")
+ for a in ans:
+ sys.stdout.write(a.delta)
+ sys.stdout.flush()
+ time.sleep(0.1)
+ self.logger.info("-----end-----")
+
+ def test_async_llm_messages(self):
+
+ messages = [Message(role="system", content="you are a helpful assistant."),
+ Message(role="user", content="你如何看待黄金上涨?")]
+
+ ans = asyncio.run(self.llm.async_call(messages=messages))
+ self.logger.info(ans.message.content)
diff --git a/memoryscope/tests/models/test_models_lli_rank.py b/memoryscope/tests/models/test_models_lli_rank.py
new file mode 100644
index 00000000..e0e90c7c
--- /dev/null
+++ b/memoryscope/tests/models/test_models_lli_rank.py
@@ -0,0 +1,34 @@
+import asyncio
+import unittest
+
+from memoryscope.core.models.llama_index_rank_model import LlamaIndexRankModel
+
+
+class TestLLIReRank(unittest.TestCase):
+ """Tests for LlamaIndexRerankModel"""
+
+ def setUp(self):
+ config = {
+ "module_name": "dashscope_rank",
+ "model_name": "gte-rerank",
+ "clazz": "models.llama_index_rerank_model"
+ }
+ self.reranker = LlamaIndexRankModel(**config)
+
+ def test_rerank(self):
+ query = "吃啥?"
+ documents = ["您吃了吗?",
+ "吃了吗您?"]
+ result = self.reranker.call(
+ documents=documents,
+ query=query)
+ print(result)
+
+ def test_async_rerank(self):
+ query = "吃啥?"
+ documents = ["您吃了吗?",
+ "吃了吗您?"]
+ result = asyncio.run(self.reranker.async_call(
+ documents=documents,
+ query=query))
+ print(result)
diff --git a/memoryscope/tests/other/init_test.py b/memoryscope/tests/other/init_test.py
new file mode 100644
index 00000000..a6c3649d
--- /dev/null
+++ b/memoryscope/tests/other/init_test.py
@@ -0,0 +1,10 @@
+def validate_path():
+ import os, sys
+
+ os.path.dirname(__file__)
+ root_dir_assume = os.path.abspath(os.path.dirname(__file__) + "/../..")
+ os.chdir(root_dir_assume)
+ sys.path.append(root_dir_assume)
+
+
+validate_path() # validate path so you can run from base directory
diff --git a/memoryscope/tests/other/read_prompt.yaml b/memoryscope/tests/other/read_prompt.yaml
new file mode 100644
index 00000000..efb0e419
--- /dev/null
+++ b/memoryscope/tests/other/read_prompt.yaml
@@ -0,0 +1,3 @@
+a:
+ cn: c
+ en: e
\ No newline at end of file
diff --git a/memoryscope/tests/other/read_yaml.py b/memoryscope/tests/other/read_yaml.py
new file mode 100644
index 00000000..ac0513f4
--- /dev/null
+++ b/memoryscope/tests/other/read_yaml.py
@@ -0,0 +1,11 @@
+import sys
+
+sys.path.append(".") # noqa: E402
+
+from memoryscope.core.utils.prompt_handler import PromptHandler
+
+if __name__ == "__main__":
+ file_path: str = __file__
+ print(file_path)
+ handler = PromptHandler(__file__, language="cn", prompt_file="read_prompt", )
+ print(handler.prompt_dict)
diff --git a/memoryscope/tests/other/test_attr.py b/memoryscope/tests/other/test_attr.py
new file mode 100644
index 00000000..eaf2fbf8
--- /dev/null
+++ b/memoryscope/tests/other/test_attr.py
@@ -0,0 +1,15 @@
+class MyClass:
+ def __init__(self):
+ self.existing_attribute = "I exist"
+
+ def do(self, name: str, **kwargs):
+ print("do %s %s" % (name, kwargs))
+
+ def __getattr__(self, name):
+ return lambda **kwargs: self.do(name, **kwargs)
+
+
+# 创建类的实例
+obj = MyClass()
+
+obj.haha(a=1, b=2)
diff --git a/memoryscope/tests/other/test_cli.py b/memoryscope/tests/other/test_cli.py
new file mode 100644
index 00000000..89a99f17
--- /dev/null
+++ b/memoryscope/tests/other/test_cli.py
@@ -0,0 +1,14 @@
+import fire
+
+
+class CLI:
+ def run(self, **kwargs):
+ """
+ 打印传入的 kwargs
+ """
+ for key, value in kwargs.items():
+ print(f"{key}: {value}")
+
+
+if __name__ == '__main__':
+ fire.Fire(CLI().run)
diff --git a/memoryscope/tests/storages/test_storages_lli_es.py b/memoryscope/tests/storages/test_storages_lli_es.py
new file mode 100644
index 00000000..a72d1aab
--- /dev/null
+++ b/memoryscope/tests/storages/test_storages_lli_es.py
@@ -0,0 +1,189 @@
+import unittest
+
+from memoryscope.core.models.llama_index_embedding_model import LlamaIndexEmbeddingModel
+from memoryscope.core.storage.llama_index_es_memory_store import LlamaIndexEsMemoryStore
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class TestLlamaIndexElasticSearchStore(unittest.TestCase):
+ """Tests for LLIEmbedding"""
+
+ def setUp(self):
+ config = {
+ "module_name": "dashscope_embedding",
+ "model_name": "text-embedding-v2",
+ "clazz": "models.llama_index_embedding_model",
+ }
+ emb = LlamaIndexEmbeddingModel(**config)
+
+ config = {
+ "index_name": "0708_2",
+ "es_url": "http://localhost:9200",
+ "embedding_model": emb,
+ "use_hybrid": True
+
+ }
+ self.es_store = LlamaIndexEsMemoryStore(**config)
+ self.data = [
+ MemoryNode(
+ content="The lives of two mob hitmen, a boxer, a gangster and his wife, "
+ "and a pair of diner bandits intertwine in four tales of violence and redemption.",
+ memory_type="observation",
+ user_id="0",
+ status="valid",
+ memory_id="aaa123",
+
+ ),
+ MemoryNode(
+ content="When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, "
+ "Batman must accept one of the greatest psychological and physical tests of his "
+ "ability to fight injustice.",
+ memory_type="observation",
+ user_id="1",
+ status="valid",
+ memory_id="bbb456",
+ meta_data={"1": "1"}
+ ),
+ MemoryNode(
+ content="An insomniac office worker and a devil-may-care soapmaker form an underground fight "
+ "club that evolves into something much, much more.",
+ memory_type="insights",
+ user_id="2",
+ status="valid",
+ memory_id="ccc789",
+ meta_data={"2": "2"}
+ ),
+ MemoryNode(
+ content="A thief who steals corporate secrets through the use of dream-sharing technology "
+ "is given the inverse task of planting an idea into thed of a C.E.O.",
+ memory_type="insights",
+ user_id="3",
+ status="valid",
+ memory_id="ddd012",
+ meta_data={"3": "3"}
+
+ ),
+ MemoryNode(
+ content="A computer hacker learns from mysterious rebels about the true nature of his reality "
+ "and his role in the war against its controllers.",
+ memory_type="profile",
+ user_id="4",
+ status="valid",
+ memory_id="eee345",
+ meta_data={"4": "4"}
+
+ ),
+ MemoryNode(
+ content="Two detectives, a rookie and a veteran, hunt a serial killer who uses the seven "
+ "deadly sins as his motives.",
+ memory_type="profile",
+ user_id="5",
+ status="valid",
+ memory_id="fff678",
+ meta_data={"5": "5"},
+
+ ),
+ MemoryNode(
+ content="An organized crime dynasty's aging patriarch transfers control of his clandestine "
+ "empire to his reluctant son.",
+ memory_type="insights",
+ user_id="6",
+ status="valid",
+ memory_id="ggg901",
+ meta_data={"5": "5"}
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="ggg234",
+ meta_data={"5": "5"}
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="hhh234",
+ meta_data={"5": "5"}
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="iii234",
+ meta_data={"5": "5"}
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="jjj234",
+ meta_data={"5": "5"}
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="kkk234",
+ meta_data={"5": "5"}
+
+ ),
+ ]
+
+ def test_retrieve(self):
+ # filter_dict = {
+ # "user_id": "6",
+ # }
+ filter_dict = {}
+
+ for node in self.data:
+ self.es_store.insert(node)
+
+ self.es_store.insert(MemoryNode(
+ content="xxxxxx",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="ggg567",
+ meta_data={"5": "5"}
+ ))
+ res = self.es_store.retrieve_memories(query="hacker", filter_dict=filter_dict, top_k=15)
+ print(len(res))
+ print(res)
+
+ self.es_store.update(MemoryNode(
+ content="test update",
+ memory_type="profile",
+ user_id="6",
+ status="invalid",
+ memory_id="ggg567"
+ ))
+ res = self.es_store.retrieve_memories(query="hacker", filter_dict=filter_dict, top_k=15)
+ print(len(res))
+ print(res)
+
+ self.es_store.delete(MemoryNode(
+ content="test update",
+ memory_type="profile",
+ user_id="6",
+ status="invalid",
+ memory_id="ggg567"
+ ))
+ import asyncio
+ res = asyncio.run(self.es_store.a_retrieve_memories(query="hacker", filter_dict=filter_dict, top_k=15))
+ # res = self.es_store.async_retrieve(query="hacker", filter_dict=filter_dict, top_k=10)
+ print(len(res))
+ print(res)
+
+ def tearDown(self):
+ self.es_store.close()
diff --git a/memoryscope/tests/storages/test_storages_lli_synces.py b/memoryscope/tests/storages/test_storages_lli_synces.py
new file mode 100644
index 00000000..f407a389
--- /dev/null
+++ b/memoryscope/tests/storages/test_storages_lli_synces.py
@@ -0,0 +1,188 @@
+import unittest
+
+from memoryscope.core.models.llama_index_embedding_model import LlamaIndexEmbeddingModel
+from memoryscope.core.storage.llama_index_es_memory_store import LlamaIndexEsMemoryStore
+from memoryscope.scheme.memory_node import MemoryNode
+
+
+class TestLlamaIndexElasticSearchStore(unittest.TestCase):
+ """Tests for LLIEmbedding"""
+
+ def setUp(self):
+ config = {
+ "module_name": "dashscope_embedding",
+ "model_name": "text-embedding-v2",
+ "clazz": "models.llama_index_embedding_model",
+ }
+ emb = LlamaIndexEmbeddingModel(**config)
+
+ config = {
+ "index_name": "0708_8",
+ "es_url": "http://localhost:9200",
+ "embedding_model": emb,
+ "retrieve_mode": "dense",
+
+ }
+ self.es_store = LlamaIndexEsMemoryStore(**config)
+ self.data = [
+ MemoryNode(
+ content="The lives of two mob hitmen, a boxer, a gangster and his wife, "
+ "and a pair of diner bandits intertwine in four tales of violence and redemption.",
+ memory_type="observation",
+ user_id="0",
+ status="valid",
+ memory_id="aaa123",
+ timestamp=1,
+
+ ),
+ MemoryNode(
+ content="When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, "
+ "Batman must accept one of the greatest psychological and physical tests of his "
+ "ability to fight injustice.",
+ memory_type="observation",
+ user_id="1",
+ status="valid",
+ memory_id="bbb456",
+ meta_data={"1": "1"},
+ timestamp=2,
+ ),
+ MemoryNode(
+ content="An insomniac office worker and a devil-may-care soapmaker form an underground fight "
+ "club that evolves into something much, much more.",
+ memory_type="insights",
+ user_id="2",
+ status="valid",
+ memory_id="ccc789",
+ meta_data={"2": "2"},
+ timestamp=3,
+ ),
+ MemoryNode(
+ content="A thief who steals corporate secrets through the use of dream-sharing technology "
+ "is given the inverse task of planting an idea into thed of a C.E.O.",
+ memory_type="insights",
+ user_id="3",
+ status="valid",
+ memory_id="ddd012",
+ meta_data={"3": "3"},
+ timestamp=4,
+
+ ),
+ MemoryNode(
+ content="A computer hacker learns from mysterious rebels about the true nature of his reality "
+ "and his role in the war against its controllers.",
+ memory_type="profile",
+ user_id="4",
+ status="valid",
+ memory_id="eee345",
+ meta_data={"4": "4"},
+ timestamp=5,
+
+ ),
+ MemoryNode(
+ content="Two detectives, a rookie and a veteran, hunt a serial killer who uses the seven "
+ "deadly sins as his motives.",
+ memory_type="profile",
+ user_id="5",
+ status="valid",
+ memory_id="fff678",
+ meta_data={"5": "5"},
+ timestamp=6,
+
+ ),
+ MemoryNode(
+ content="An organized crime dynasty's aging patriarch transfers control of his clandestine "
+ "empire to his reluctant son.",
+ memory_type="insights",
+ user_id="6",
+ status="valid",
+ memory_id="ggg901",
+ meta_data={"5": "5"},
+ timestamp=7,
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="ggg234",
+ meta_data={"5": "5"},
+ timestamp=8,
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="hhh234",
+ meta_data={"5": "5"},
+ timestamp=9,
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="iii234",
+ meta_data={"5": "5"},
+ timestamp=10,
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="jjj234",
+ meta_data={"5": "5"},
+ timestamp=11,
+
+ ),
+ MemoryNode(
+ content="ggggggggg",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="kkk234",
+ meta_data={"5": "5"},
+ timestamp=12,
+
+ ),
+ ]
+
+ for node in self.data:
+ self.es_store.insert(node)
+
+ self.es_store.insert(MemoryNode(
+ content="xxxxxx",
+ memory_type="profile",
+ user_id="6",
+ status="valid",
+ memory_id="ggg567",
+ meta_data={"5": "5"},
+ timestamp=13
+ ))
+
+ def test_retrieve(self):
+ filter_dict = {
+ "timestamp": 12,
+ # "memory_id": "bbb456",
+ # "score_rank": 0,
+ }
+
+ res = self.es_store.retrieve_memories(query="hacker", filter_dict=filter_dict, top_k=15)
+ print(len(res))
+ print(res)
+
+ def test_retrieve_wo_query(self, ):
+ filter_dict = {
+ "memory_id": "bbb456",
+ }
+ res = self.es_store.retrieve_memories(filter_dict=filter_dict, top_k=15)
+ print(len(res))
+ print(res)
+
+ def tearDown(self):
+ self.es_store.close()
diff --git a/memoryscope/tests/worker/test_workers_cn.py b/memoryscope/tests/worker/test_workers_cn.py
new file mode 100644
index 00000000..21c0cebb
--- /dev/null
+++ b/memoryscope/tests/worker/test_workers_cn.py
@@ -0,0 +1,428 @@
+import datetime
+import unittest
+
+from memoryscope.constants.common_constants import NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES, \
+ MERGE_OBS_NODES, QUERY_WITH_TS, EXTRACT_TIME_DICT, NOT_REFLECTED_NODES, INSIGHT_NODES, NOT_UPDATED_NODES, \
+ MEMORYSCOPE_CONTEXT, CHAT_MESSAGES_SCATTER, TARGET_NAME
+from memoryscope.core.config.arguments import Arguments
+from memoryscope.core.memoryscope import MemoryScope
+from memoryscope.core.utils.tool_functions import init_instance_by_config
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.scheme.memory_node import MemoryNode
+from memoryscope.scheme.message import Message
+
+
+class TestWorkersCn(unittest.TestCase):
+ """Tests for LLIEmbedding"""
+
+ def setUp(self):
+ self.arguments = Arguments(
+ language="cn",
+ human_name="用户",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="dashscope_generation",
+ generation_model="qwen-max",
+ embedding_backend="dashscope_embedding",
+ embedding_model="text-embedding-v2",
+ rank_backend="dashscope_rank",
+ rank_model="gte-rerank",
+ enable_ranker=True,
+ )
+ self.ms = MemoryScope(arguments=self.arguments)
+
+ def tearDown(self):
+ self.ms.close()
+
+ @unittest.skip
+ def test_extract_time(self):
+ name = "extract_time"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ query = "明天我去上海出差"
+ query_timestamp = int(datetime.datetime.now().timestamp())
+ worker.set_workflow_context(QUERY_WITH_TS, (query, query_timestamp))
+ worker.run()
+
+ result = worker.get_workflow_context(EXTRACT_TIME_DICT)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_info_filter(self):
+ name = "info_filter"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="我爱吃川菜", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我爱中国", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="假设我是秦始皇", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="帮我写个作文", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="肾病怎么治", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我爱吃海鲜", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我不喜欢吃苹果", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="明天我要去高考", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="明天我要去北京出差", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我在阿里巴巴工作", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我擅长优化百炼算法模型",
+ role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = [msg.content for msg in worker.chat_messages_scatter]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_info_filter2(self):
+ name = "info_filter"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="你知道北京哪里的海鲜最新鲜吗",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="有没有推荐的策略游戏?最近想找新的挑战。",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="听说篮球运动对身体很好,是真的吗?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="最近在北京的工作压力太大,有什么放松的建议吗?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="说到朋友,我确实有几位很要好的朋友,我们经常一起出去吃饭。",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="对了,最近想换工作,你觉得北京的哪个区工作机会更多?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="听你这么说,我感觉挺有信心的,谢了!",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我很喜欢尝试新的美食,有没有推荐的美食应用?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我有时也喜欢自己在家做饭,你有没有好的海鲜菜谱推荐?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="听说打篮球可以长高,这是真的吗?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我在北京阿里云园区工作",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我是阿里云百炼的工程师",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="最后一个问题,你知道怎么才能维持广泛的社交关系吗?",
+ role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = [msg.content for msg in worker.chat_messages_scatter]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation(self):
+ name = "get_observation"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="我爱吃川菜", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我不喜欢吃苹果", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我准备去高考", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我不喜欢吃西瓜", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我不爱吃西瓜", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我在一家叫京东的公司干活",
+ role_name=self.arguments.human_name),
+ ]
+
+ # chat_messages = [
+ # Message(role=MessageRoleEnum.USER.value, content="我不喜欢吃西瓜"),
+ # Message(role=MessageRoleEnum.USER.value, content="我在一家叫京东的公司干活"),
+ # ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation2(self):
+ name = "get_observation"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="有没有推荐的策略游戏?最近想找新的挑战。",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="最近在北京的工作压力太大,有什么放松的建议吗?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="说到朋友,我确实有几位很要好的朋友,我们经常一起出去吃饭。",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="对了,最近想换工作,你觉得北京的哪个区工作机会更多?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我很喜欢尝试新的美食,有没有推荐的美食应用?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我有时也喜欢自己在家做饭,你有没有好的海鲜菜谱推荐?",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我在北京阿里云园区工作",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="我是阿里云百炼的工程师",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="最后一个问题,你知道怎么才能维持广泛的社交关系吗?",
+ role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation_with_time(self):
+ name = "get_observation_with_time"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="去年我们一起合作了因果推断技术",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="上个月我去了杭州旅游",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="下周我要去高考", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="明天我去北京出差", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="前天我把苹果扔掉了", role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="前天我把苹果扔掉了,我不喜欢吃",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.USER.value, content="明天是我生日", role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_WITH_TIME_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_contra_repeat(self):
+ name = "contra_repeat"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="用户在美团干活"),
+ MemoryNode(user_name="AI", target_name="用户", content="用户在阿里巴巴工作"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result1 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="用户在京东工作"),
+ MemoryNode(user_name="AI", target_name="用户", content="用户在美团干活"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result2 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="用户在京东工作或有工作经验。"),
+ MemoryNode(user_name="AI", target_name="用户", content="用户跳槽至openai工作。"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result3 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="我喜欢吃西瓜"),
+ MemoryNode(user_name="AI", target_name="用户", content="用户在阿里巴巴干活"),
+ MemoryNode(user_name="AI", target_name="用户", content="我不爱吃西瓜"),
+ MemoryNode(user_name="AI", target_name="用户", content="我喜欢吃葡萄"),
+ MemoryNode(user_name="AI", target_name="用户", content="我爱吃苹果和香蕉"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result4 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ worker.logger.info(f"result1={result1}")
+ worker.logger.info(f"result2={result2}")
+ worker.logger.info(f"result3={result3}")
+ worker.logger.info(f"result4={result4}")
+
+ @unittest.skip
+ def test_get_reflection_subject(self):
+ name = "get_reflection_subject"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="用户对策略游戏感兴趣,寻找新挑战。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户在北京工作,感到压力大,寻求放松方式。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户有要好朋友,常一起外出就餐。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户打算换工作,关心北京的工作机会分布。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户喜爱尝试新美食,求美食应用推荐。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户喜欢在家做饭,寻求海鲜菜谱。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户在北京阿里云园区工作。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户是阿里云百炼的工程师。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户目前的工作是大语言模型的应用开发", role_name=self.arguments.human_name),
+ MemoryNode(content="用户想知道维持广泛社交关系的方法。", role_name=self.arguments.human_name),
+ ]
+
+ worker.memory_manager.set_memories(NOT_REFLECTED_NODES, nodes)
+ worker.memory_manager.set_memories(INSIGHT_NODES, [])
+ worker.run()
+
+ result = [node.key for node in worker.memory_manager.get_memories(INSIGHT_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.get_reflection={result}")
+ return worker
+
+ @unittest.skip
+ def test_update_insight_worker(self):
+ reflection_worker: MemoryBaseWorker = self.test_get_reflection_subject.__wrapped__(self)
+
+ name = "update_insight"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context=reflection_worker.context,
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="用户喜欢打王者荣耀", role_name=self.arguments.human_name),
+ ]
+ worker.memory_manager.set_memories(NOT_UPDATED_NODES, nodes)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(INSIGHT_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.update_insight={result}")
+
+ @unittest.skip
+ def test_long_contra_repeat_worker(self):
+ name = "long_contra_repeat"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="用户对策略游戏感兴趣,寻找新挑战。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户在北京工作,感到压力大,寻求放松方式。", role_name=self.arguments.human_name),
+ MemoryNode(content="用户在上海工作。", role_name=self.arguments.human_name),
+ ]
+ worker.memory_manager.set_memories(NOT_UPDATED_NODES, nodes)
+ worker.unit_test_flag = True
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.long_contra_repeat={result}")
+
+ # @unittest.skip
+ def test_example_query_worker(self):
+ name = "example_query_worker"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config={
+ "class": "contrib.example_query_worker",
+ "generation_model": "generation_model",
+ },
+ name=name,
+ context={MEMORYSCOPE_CONTEXT: self.ms._context, TARGET_NAME: self.arguments.human_name,
+ "chat_kwargs": {"query": "我一直很爱他们"}},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms._context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="我的两个孩子分别叫小明和小红",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.ASSISTANT.value,
+ content="很高兴认识您和您的家庭成员!小明和小红是非常通俗且好听的名字。",
+ role_name=self.arguments.assistant_name),
+ Message(role=MessageRoleEnum.USER.value, content="我一直很爱他们", role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = worker.get_workflow_context(QUERY_WITH_TS)
+ worker.logger.info(f"result={result}")
diff --git a/memoryscope/tests/worker/test_workers_en.py b/memoryscope/tests/worker/test_workers_en.py
new file mode 100644
index 00000000..44dca900
--- /dev/null
+++ b/memoryscope/tests/worker/test_workers_en.py
@@ -0,0 +1,416 @@
+import datetime
+import unittest
+
+from memoryscope.constants.common_constants import CHAT_MESSAGES, NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES, \
+ MERGE_OBS_NODES, QUERY_WITH_TS, EXTRACT_TIME_DICT, NOT_REFLECTED_NODES, INSIGHT_NODES, NOT_UPDATED_NODES, \
+ MEMORYSCOPE_CONTEXT, TARGET_NAME, CHAT_MESSAGES_SCATTER
+from memoryscope.core.config.arguments import Arguments
+from memoryscope.core.memoryscope import MemoryScope
+from memoryscope.core.utils.tool_functions import init_instance_by_config
+from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
+from memoryscope.enumeration.message_role_enum import MessageRoleEnum
+from memoryscope.scheme.memory_node import MemoryNode
+from memoryscope.scheme.message import Message
+
+
+class TestWorkersEn(unittest.TestCase):
+ """Tests for LLIEmbedding"""
+
+ def setUp(self):
+ self.arguments = Arguments(
+ language="en",
+ human_name="user",
+ assistant_name="AI",
+ memory_chat_class="api_memory_chat",
+ generation_backend="dashscope_generation",
+ generation_model="qwen-max",
+ embedding_backend="dashscope_embedding",
+ embedding_model="text-embedding-v2",
+ rank_backend="dashscope_rank",
+ rank_model="gte-rerank",
+ )
+ self.ms = MemoryScope(arguments=self.arguments)
+
+ def tearDown(self):
+ self.ms.close()
+
+ @unittest.skip
+ def test_extract_time(self):
+ name = "extract_time"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ query = "I will be on a business trip to Shanghai tomorrow."
+ query_timestamp = int(datetime.datetime.now().timestamp())
+ worker.set_workflow_context(QUERY_WITH_TS, (query, query_timestamp))
+ worker.run()
+
+ result = worker.get_workflow_context(EXTRACT_TIME_DICT)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_info_filter(self):
+ name = "info_filter"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="I love to eat Sichuan cuisine."),
+ Message(role=MessageRoleEnum.USER.value, content="I don't like eating apples."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I'm going to take the college entrance examination tomorrow."),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES, chat_messages)
+ worker.run()
+
+ result = [msg.content for msg in worker.chat_messages_scatter]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_info_filter2(self):
+ name = "info_filter"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="Do you know where the freshest seafood is in Beijing?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="Are there any strategy games you would recommend? I'm looking for a new challenge."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I heard that basketball is good for your health, is that true?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I’ve been under a lot of work pressure in Beijing lately. Do you have any "
+ "suggestions for relaxing?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="Speaking of friends, I do have a few very good friends and we often go out "
+ "to eat together."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="By the way, I want to change jobs recently. Which district in Beijing do you think "
+ "has more job opportunities?"),
+ Message(role=MessageRoleEnum.USER.value, content="Hearing you say that, I feel more confident. Thank you!"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I love trying new food, are there any food apps you would recommend?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I also like to cook at home sometimes. Do you have any good seafood recipes to "
+ "recommend?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I heard that playing basketball can help you grow taller, is this true?"),
+ Message(role=MessageRoleEnum.USER.value, content="I work at Alibaba Cloud in Beijing"),
+ Message(role=MessageRoleEnum.USER.value, content="I am an engineer at Alibaba Cloud Bailian"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="Last question, do you know how to maintain extensive social relationships?"),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES, chat_messages)
+ worker.run()
+
+ result = [msg.content for msg in worker.chat_messages_scatter]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation(self):
+ name = "get_observation"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ # FIXME Does the appearance of 'am' indicate the presence of a time keyword?
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="I love Sichuan cuisine"),
+ Message(role=MessageRoleEnum.USER.value, content="I don't like eating apples"),
+ Message(role=MessageRoleEnum.USER.value, content="I am preparing for the college entrance examination"),
+ Message(role=MessageRoleEnum.USER.value, content="I don't like eating watermelon"),
+ Message(role=MessageRoleEnum.USER.value, content="I don't like watermelon"),
+ Message(role=MessageRoleEnum.USER.value, content="I work for a company called JD.com"),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation2(self):
+ name = "get_observation"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value,
+ content="Are there any strategy games you would recommend? I'm looking for a new challenge."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I’ve been under a lot of work pressure in Beijing lately. Do you have any suggestions "
+ "for relaxing?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="Speaking of friends, I do have a few very good friends and we often go out to "
+ "eat together."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="By the way, I want to change jobs recently. Which district in Beijing do you think has "
+ "more job opportunities?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I love trying new food, are there any food apps you would recommend?"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I also like to cook at home sometimes. Do you have any good seafood recipes to "
+ "recommend?"),
+ Message(role=MessageRoleEnum.USER.value, content="I work at Alibaba Cloud in Beijing"),
+ Message(role=MessageRoleEnum.USER.value, content="I am an engineer at Alibaba Cloud Bailian"),
+ Message(role=MessageRoleEnum.USER.value,
+ content="Last question, do you know how to maintain extensive social relationships?"),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_get_observation_with_time(self):
+ name = "get_observation_with_time"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value,
+ content="Last year we worked together on causal inference technology."),
+ Message(role=MessageRoleEnum.USER.value, content="Last month I went to Hangzhou for a trip."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="I'm going to take the college entrance examination next week."),
+ Message(role=MessageRoleEnum.USER.value, content="I'm going to Beijing on a business trip tomorrow."),
+ Message(role=MessageRoleEnum.USER.value, content="I threw away the apple the day before yesterday."),
+ Message(role=MessageRoleEnum.USER.value,
+ content="The day before yesterday I threw away the apple. I don't like eating it."),
+ Message(role=MessageRoleEnum.USER.value, content="Tomorrow is my birthday."),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES, chat_messages)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(NEW_OBS_WITH_TIME_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result={result}")
+
+ @unittest.skip
+ def test_contra_repeat(self):
+ name = "contra_repeat"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="User is working in Meituan"),
+ MemoryNode(user_name="AI", target_name="用户", content="User works at Alibaba"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result1 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="User works at JD.com"),
+ MemoryNode(user_name="AI", target_name="用户", content="Users working in Meituan"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result2 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="User works at JD.com"),
+ MemoryNode(user_name="AI", target_name="用户", content="User is working in Meituan"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result3 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ nodes = [
+ MemoryNode(user_name="AI", target_name="用户", content="I like to eat watermelon"),
+ MemoryNode(user_name="AI", target_name="用户", content="User is working in Alibaba"),
+ MemoryNode(user_name="AI", target_name="用户", content="I don't like watermelon"),
+ ]
+
+ worker.memory_manager.set_memories(NEW_OBS_NODES, nodes)
+ worker.run()
+ result4 = "\n".join([" ".join([node.content, node.store_status, node.action_status])
+ for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)])
+
+ worker.logger.info(f"result1={result1}")
+ worker.logger.info(f"result2={result2}")
+ worker.logger.info(f"result3={result3}")
+ worker.logger.info(f"result4={result4}")
+
+ @unittest.skip
+ def test_get_reflection_subject(self):
+ name = "get_reflection_subject"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="Users are interested in strategy games and looking for new challenges."),
+ MemoryNode(content="The user works in Beijing, feels stressed, and is looking for ways to relax."),
+ MemoryNode(content="The user has good friends and often goes out to eat together."),
+ MemoryNode(content="The user is planning to change jobs and is concerned about the distribution of job "
+ "opportunities in Beijing."),
+ MemoryNode(content="Users love to try new food and ask for food app recommendations."),
+ MemoryNode(content="Users love to cook at home and seek seafood recipes."),
+ MemoryNode(content="The user works in the Alibaba Cloud campus in Beijing."),
+ MemoryNode(content="The user is an engineer at Alibaba Cloud Bailian."),
+ MemoryNode(content="The user's current job is application development of large language models"),
+ MemoryNode(content="Users want to know how to maintain extensive social relationships."),
+ ]
+
+ worker.memory_manager.set_memories(NOT_REFLECTED_NODES, nodes)
+ worker.memory_manager.set_memories(INSIGHT_NODES, [])
+ worker.run()
+
+ result = [node.key for node in worker.memory_manager.get_memories(INSIGHT_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.get_reflection={result}")
+ return worker
+
+ @unittest.skip
+ def test_update_insight_worker(self):
+ reflection_worker: MemoryBaseWorker = self.test_get_reflection_subject.__wrapped__(self)
+
+ name = "update_insight"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context=reflection_worker.context,
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="Users like to play King of Glory"),
+ ]
+ worker.memory_manager.set_memories(NOT_UPDATED_NODES, nodes)
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(INSIGHT_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.update_insight={result}")
+
+ # @unittest.skip
+ def test_long_contra_repeat_worker(self):
+ name = "long_contra_repeat"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config=self.ms.context.worker_conf_dict[name],
+ name=name,
+ is_multi_thread=False,
+ context={MEMORYSCOPE_CONTEXT: self.ms.context, TARGET_NAME: self.arguments.human_name},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms.context.thread_pool)
+
+ nodes = [
+ MemoryNode(content="Users are interested in strategy games and looking for new challenges."),
+ MemoryNode(content="The user works in Beijing, feels stressed, and is looking for ways to relax."),
+ MemoryNode(content="User works in Shanghai."),
+ ]
+ worker.memory_manager.set_memories(NOT_UPDATED_NODES, nodes)
+ worker.unit_test_flag = True
+ worker.run()
+
+ result = [node.content for node in worker.memory_manager.get_memories(MERGE_OBS_NODES)]
+ result = "\n".join(result)
+ worker.logger.info(f"result.long_contra_repeat={result}")
+
+ # @unittest.skip
+ def test_example_query_worker(self):
+ name = "example_query_worker"
+
+ worker: MemoryBaseWorker = init_instance_by_config(
+ config={
+ "class": "contrib.example_query_worker",
+ "generation_model": "generation_model",
+ },
+ name=name,
+ context={MEMORYSCOPE_CONTEXT: self.ms._context, TARGET_NAME: self.arguments.human_name,
+ "chat_kwargs": {"query": "I have always loved them."}},
+ context_lock=None,
+ memoryscope_context=self.ms.context,
+ thread_pool=self.ms._context.thread_pool)
+
+ chat_messages = [
+ Message(role=MessageRoleEnum.USER.value, content="My two children are named Xiaoming and Xiaohong.",
+ role_name=self.arguments.human_name),
+ Message(role=MessageRoleEnum.ASSISTANT.value,
+ content="I am very pleased to meet you and your family members! Xiaoming and Xiaohong are very pleasant names.",
+ role_name=self.arguments.assistant_name),
+ Message(role=MessageRoleEnum.USER.value, content="I have always loved them.",
+ role_name=self.arguments.human_name),
+ ]
+
+ worker.set_workflow_context(CHAT_MESSAGES_SCATTER, chat_messages)
+ worker.run()
+
+ result = worker.get_workflow_context(QUERY_WITH_TS)
+ worker.logger.info(f"result={result}")