diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..69361c03
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+.vscode
+.env
+.DS_Store
+.idea
+venv/
+.ipynb_checkpoints
+.__pycache__
+__pycache__
+*.log
+tmp*
+temp*
+private*
+dist/
+nohup*
+cache
+log/
+.trash/
+runs
+logs
+alfworld_data
+beyondagent/dataset/appworld/data
diff --git a/README.md b/README.md
index e69de29b..d96575d9 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,42 @@
+# run service
+
+```shell
+cd BeyondAgent
+python beyondagent/core/service/model_service.py
+```
+
+# test service
+
+```shell
+python beyondagent/test/test_service.py
+```
+
+# qingxu
+```shell
+# 1. edit query, port, vm etc
+nano docker-compose.yml
+# 2. run
+docker compose down && docker compose build && docker compose up
+# 3. then watch vm at http://localhost:16901 (default password is headless)
+```
+
+
+# vector store
+If a vector database is involved, you will need an Elasticsearch environment. You can refer to the following steps:
+- If you don’t have Docker installed, download and install [Docker Desktop](https://www.docker.com/products/docker-desktop) for your operating system.
+- To set up [Elasticsearch](https://www.elastic.co/docs/solutions/search/run-elasticsearch-locally) and Kibana locally, run the start-local script in the command line:
+```shell
+curl -fsSL https://elastic.co/start-local | sh
+```
+
+Or manually download and load the image. Here, we take elasticsearch-wolfi:9.0.0 as an example:
+```shell
+docker pull docker.elastic.co/elasticsearch/elasticsearch-wolfi:9.0.0
+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-wolfi:9.0.0
+```
+
+# run module service
diff --git a/cookbook/__init__.py b/cookbook/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/__init__.py b/experiencescope/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/enumeration/__init__.py b/experiencescope/enumeration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/enumeration/agent_state.py b/experiencescope/enumeration/agent_state.py
new file mode 100644
index 00000000..ae7269a0
--- /dev/null
+++ b/experiencescope/enumeration/agent_state.py
@@ -0,0 +1,8 @@
+from enum import Enum
+
+
+class AgentState(str, Enum):
+ IDLE = "idle"
+ RUNNING = "running"
+ COMPLETE = "complete"
+ FAILED = "failed"
diff --git a/experiencescope/enumeration/chunk_enum.py b/experiencescope/enumeration/chunk_enum.py
new file mode 100644
index 00000000..975f0b1f
--- /dev/null
+++ b/experiencescope/enumeration/chunk_enum.py
@@ -0,0 +1,9 @@
+from enum import Enum
+
+
+class ChunkEnum(str, Enum):
+ THINK = "think"
+ ANSWER = "answer"
+ TOOL = "tool"
+ USAGE = "usage"
+ ERROR = "error"
diff --git a/experiencescope/enumeration/http_enum.py b/experiencescope/enumeration/http_enum.py
new file mode 100644
index 00000000..69354c45
--- /dev/null
+++ b/experiencescope/enumeration/http_enum.py
@@ -0,0 +1,6 @@
+from enum import Enum
+
+
+class HttpEnum(str, Enum):
+ GET = "get"
+ POST = "post"
diff --git a/experiencescope/enumeration/role.py b/experiencescope/enumeration/role.py
new file mode 100644
index 00000000..ae33f0e0
--- /dev/null
+++ b/experiencescope/enumeration/role.py
@@ -0,0 +1,11 @@
+from enum import Enum
+
+
+class Role(str, Enum):
+ SYSTEM = "system"
+ USER = "user"
+ TOOL = "tool" # environment
+
+ ASSISTANT = "assistant" # policy model
+ CONTEXT_ASSISTANT = "context_assistant" # context model
+ SUMMARY_ASSISTANT = "summary_assistant" # summary model
diff --git a/experiencescope/model/__init__.py b/experiencescope/model/__init__.py
new file mode 100644
index 00000000..cf37ad31
--- /dev/null
+++ b/experiencescope/model/__init__.py
@@ -0,0 +1,10 @@
+from beyondagent.core.model.openai_compatible_embedding_model import OpenAICompatibleEmbeddingModel
+from beyondagent.core.model.openai_compatible_llm import OpenAICompatibleBaseLLM
+
+from beyondagent.core.utils.registry import Registry
+
+LLM_REGISTRY = Registry("llm")
+LLM_REGISTRY.register(OpenAICompatibleBaseLLM, "openai_compatible")
+
+EMBEDDING_MODEL_REGISTRY = Registry("embedding_model")
+EMBEDDING_MODEL_REGISTRY.register(OpenAICompatibleEmbeddingModel, "openai_compatible")
diff --git a/experiencescope/model/base_embedding_model.py b/experiencescope/model/base_embedding_model.py
new file mode 100644
index 00000000..b675a51c
--- /dev/null
+++ b/experiencescope/model/base_embedding_model.py
@@ -0,0 +1,86 @@
+from abc import ABC
+from typing import List
+
+from loguru import logger
+from pydantic import BaseModel, Field
+
+from beyondagent.core.schema.vector_store_node import VectorStoreNode
+
+
+class BaseEmbeddingModel(BaseModel, ABC):
+ model_name: str = Field(default=..., description="model name")
+ dimensions: int = Field(default=..., description="dimensions")
+ max_retries: int = Field(default=3, description="max retries")
+ raise_exception: bool = Field(default=True, description="raise exception")
+
+ def _get_embeddings(self, input_text: str | List[str]):
+ """
+ Get the embedding vector based on the input text.
+ This is an abstract method, and its concrete implementation must be provided in a subclass to generate the embedding vector for the given text.
+ Args:
+ input_text (str | List[str]): The input text, which can be a single string or a list of strings.
+ Raises:
+ NotImplementedError: If the method is not implemented in the subclass.
+ """
+ raise NotImplementedError
+
+ def get_embeddings(self, input_text: str | List[str]):
+ """
+ Retrieves embeddings for the input text.
+
+ This function attempts to obtain embeddings for the given input text. It will retry a maximum number of times in case of failure.
+
+ Parameters:
+ - input_text (str | List[str]): The input text, which can be a single string or a list of strings.
+
+ Returns:
+ - embeddings: The embeddings for the input text. Returns None if the maximum number of retries is reached and no successful result is obtained.
+ """
+ # Attempt to get embeddings, with a maximum number of retries set
+ for i in range(self.max_retries):
+ try:
+ # Attempt to get embeddings, return immediately if successful
+ return self._get_embeddings(input_text)
+
+ except Exception as e:
+ # Log exception information when an error occurs
+ logger.exception(f"embedding model name={self.model_name} encounter error with e={e.args}")
+
+ # If the maximum number of retries is reached and raise_exception is set to True, re-throw the exception
+ if i == self.max_retries - 1 and self.raise_exception:
+ raise e
+
+ return None
+
+ def get_node_embeddings(self, nodes: VectorStoreNode | List[VectorStoreNode]):
+ """
+ Assigns embeddings to the nodes based on their content.
+
+ This function accepts either a single VectorStoreNode or a list of VectorStoreNodes.
+ It retrieves the embedding for the content of each node and assigns it to the node's vector attribute.
+ If a list of nodes is provided, it performs a batch retrieval of embeddings.
+
+ Parameters:
+ - nodes (VectorStoreNode | List[VectorStoreNode]): A single node or list of nodes whose embeddings need to be retrieved.
+
+ Returns:
+ - (VectorStoreNode | List[VectorStoreNode]): Returns the input nodes with their vector attribute populated with embeddings.
+
+ Raises:
+ - RuntimeError: If the input is neither a VectorStoreNode nor a list of VectorStoreNodes, a RuntimeError is raised.
+ """
+ if isinstance(nodes, VectorStoreNode):
+ nodes.vector = self.get_embeddings(nodes.content)
+ return nodes
+
+ elif isinstance(nodes, list):
+ embeddings = self.get_embeddings(input_text=[node.content for node in nodes])
+ if len(embeddings) != len(nodes):
+ logger.warning(f"embeddings.size={len(embeddings)} <> nodes.size={len(nodes)}")
+ else:
+ for node, embedding in zip(nodes, embeddings):
+ node.vector = embedding
+ return nodes
+
+ else:
+ raise RuntimeError(f"unsupported type={type(nodes)}")
diff --git a/experiencescope/model/base_llm.py b/experiencescope/model/base_llm.py
new file mode 100644
index 00000000..79707652
--- /dev/null
+++ b/experiencescope/model/base_llm.py
@@ -0,0 +1,110 @@
+from abc import ABC
+from typing import List, Literal
+
+from loguru import logger
+from pydantic import Field, BaseModel
+
+from beyondagent.core.schema.trajectory import Message, ActionMessage
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class BaseLLM(BaseModel, ABC):
+ model_name: str = Field(...)
+
+ seed: int = Field(default=42)
+ top_p: float | None = Field(default=None)
+ # stream: bool = Field(default=True)
+ stream_options: dict = Field(default={"include_usage": True})
+ temperature: float = Field(default=0.0000001)
+ presence_penalty: float | None = Field(default=None)
+ enable_thinking: bool = Field(default=True, description="whether the current mode is the reasoning model, "
+ "or whether Qwen3's reasoning mode is currently enabled.")
+ tool_choice: Literal["none", "auto", "required"] = Field(default="auto", description="tool choice")
+ parallel_tool_calls: bool = Field(default=True)
+
+ max_retries: int = Field(default=3, description="max retries")
+ raise_exception: bool = Field(default=True, description="raise exception")
+
+ def stream_chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
+ """
+ This method is designed to handle streaming chat functionality, allowing for interactive communication
+ with the ability to use various tools. It is intended to be overridden by subclasses to implement
+ specific streaming chat logic.
+
+ Parameters:
+ - messages: A list of Message objects, representing the message history or current messages in the chat.
+ - tools: An optional list of BaseTool objects, representing the tools available for use during the chat.
+ - **kwargs: Additional keyword arguments for future expansion or specific implementations.
+
+ Raises:
+ - NotImplementedError: This method raises a NotImplementedError to indicate that the functionality
+ should be implemented by subclasses.
+ """
+ raise NotImplementedError
+
+ def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
+ """
+ This method is intended to be overridden by subclasses to implement specific message streaming printing logic.
+ The method raises a NotImplementedError, indicating that this is an abstract method that must be implemented by subclasses.
+
+ Parameters:
+ - messages: A list of Message objects, representing the messages to be printed.
+ - tools: An optional list of BaseTool objects, representing auxiliary tools that may be needed during the printing process.
+ - **kwargs: Additional keyword arguments, allowing for flexible handling of extra parameters.
+
+ Raises:
+ - NotImplementedError: Indicates that the method is abstract and needs to be implemented by a subclass.
+ """
+ raise NotImplementedError
+
+ def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage:
+ """
+ Abstract method for processing chat messages and generating responses.
+
+ This method is designed to be overridden by subclasses to implement specific chat logic.
+ It receives a list of messages as input, along with optional tools, and is expected to return
+ an ActionMessage object as a response. The method raises a NotImplementedError to enforce
+ implementation by subclasses.
+
+ Parameters:
+ - messages: List[Message] - A list of Message objects representing the chat history or current messages.
+ - tools: List[BaseTool] (optional) - A list of BaseTool objects representing the tools available for use during the chat. Defaults to None.
+ - **kwargs: Additional keyword arguments for extensibility and backwards compatibility.
+
+ Returns:
+ - ActionMessage: The response generated based on the input messages, encapsulated in an ActionMessage object.
+ """
+ raise NotImplementedError
+
+ def chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage | None:
+ """
+ Initiates a chat session with a model, allowing for the execution of tools.
+
+ This function sends a series of messages to the model and expects to receive an execution response.
+ It can handle exceptions during the chat process by retrying a set number of times.
+
+ Parameters:
+ - messages (List[Message]): A list of message objects, containing the conversation history.
+ - tools (List[BaseTool], optional): A list of tool objects that can be used during the chat. Defaults to None.
+ - **kwargs: Additional parameters that can be passed to the model.
+
+ Returns:
+ - ActionMessage: A response message containing the model's execution results.
+ - None: Returns None if the maximum number of retries is reached and no successful response is obtained.
+ """
+
+ # Iterate according to the maximum number of retries set
+ for i in range(self.max_retries):
+ try:
+ # Attempt to execute the chat logic
+ return self._chat(messages, tools, **kwargs)
+
+ except Exception as e:
+ # Log exceptions during the chat process
+ logger.exception(f"chat with model={self.model_name} encounter error with e={e.args}")
+
+ # If the maximum number of retries is reached and raise_exception is set to True, then re-throw the exception
+ if i == self.max_retries - 1 and self.raise_exception:
+ raise e
+
+ return None
diff --git a/experiencescope/model/openai_compatible_embedding_model.py b/experiencescope/model/openai_compatible_embedding_model.py
new file mode 100644
index 00000000..a2240539
--- /dev/null
+++ b/experiencescope/model/openai_compatible_embedding_model.py
@@ -0,0 +1,73 @@
+import os
+from typing import Literal, List
+
+from openai import OpenAI
+from pydantic import Field, PrivateAttr, model_validator
+
+from beyondagent.core.model.base_embedding_model import BaseEmbeddingModel
+
+
+class OpenAICompatibleEmbeddingModel(BaseEmbeddingModel):
+ api_key: str = Field(default_factory=lambda: os.getenv("OPENAI_API_KEY"), description="api key")
+ base_url: str = Field(default_factory=lambda: os.getenv("OPENAI_BASE_URL"), description="base url")
+ model_name: str = Field(default="text-embedding-v3", description="model name")
+ dimensions: int = Field(default=1024, description="dimensions")
+ encoding_format: Literal["float", "base64"] = Field(default="float", description="encoding_format")
+ _client: OpenAI = PrivateAttr()
+
+ @model_validator(mode="after")
+ def init_client(self):
+ """
+ Initialize the OpenAI client after model validation.
+
+ This method is called after the model's data has been validated,
+ ensuring that all necessary attributes are correctly set before
+ initializing the OpenAI client. It creates an instance of the OpenAI
+ client using the provided API key and base URL, storing it in the
+
+ Returns:
+ self: Returns the instance of the current class for method chaining.
+ """
+ self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
+ return self
+
+ def _get_embeddings(self, input_text: str | List[str]):
+ """
+ Generate embeddings for the input text.
+
+ This method accepts either a single string or a list of strings as input,
+ and returns the corresponding embeddings based on the specified model.
+
+ Parameters:
+ - input_text (str | List[str]): The input text, which can be a single string or a list of strings.
+
+ Returns:
+ - List[float]: If the input is a single string, returns a list of floating-point numbers representing the embedding.
+ - List[List[float]]: If the input is a list of strings, returns a list where each item is a list of floating-point numbers representing the embedding of each string.
+ - Raises RuntimeError: If the input type is unsupported.
+ """
+
+ # Create embeddings using the specified model, input text, dimensions, and encoding format
+ completion = self._client.embeddings.create(
+ model=self.model_name,
+ input=input_text,
+ dimensions=self.dimensions,
+ encoding_format=self.encoding_format
+ )
+
+ # Determine the type of input and process accordingly
+ if isinstance(input_text, str):
+ # If the input is a single string, return the embedding of that string
+ return completion.data[0].embedding
+
+ elif isinstance(input_text, list):
+ # If the input is a list of strings, initialize a list to hold the embeddings of each string
+ result_emb = [[] for _ in range(len(input_text))]
+ # Iterate through the generated embeddings and assign them to the corresponding positions in the result list
+ for emb in completion.data:
+ result_emb[emb.index] = emb.embedding
+ return result_emb
+
+ else:
+ # If the input type is neither a string nor a list of strings, throw an exception
+ raise RuntimeError(f"unsupported type={type(input_text)}")
diff --git a/experiencescope/model/openai_compatible_llm.py b/experiencescope/model/openai_compatible_llm.py
new file mode 100644
index 00000000..4a78587a
--- /dev/null
+++ b/experiencescope/model/openai_compatible_llm.py
@@ -0,0 +1,156 @@
+import os
+from typing import List
+
+from loguru import logger
+from openai import OpenAI
+from openai.types import CompletionUsage
+from pydantic import Field, PrivateAttr, model_validator
+
+from beyondagent.core.enumeration.chunk_enum import ChunkEnum
+from beyondagent.core.model.base_llm import BaseLLM
+from beyondagent.core.schema.trajectory import Message, ActionMessage, ToolCall
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class OpenAICompatibleBaseLLM(BaseLLM):
+ model_name: str = Field(default="qwen3-32b")
+ api_key: str = Field(default_factory=lambda: os.getenv("OPENAI_API_KEY"), description="api key")
+ base_url: str = Field(default_factory=lambda: os.getenv("OPENAI_BASE_URL"), description="base url")
+ _client: OpenAI = PrivateAttr()
+
+ @model_validator(mode="after")
+ def init_client(self):
+ """
+ Initialize the OpenAI client after model validation.
+
+ This method is called after the model's data has been validated,
+ ensuring that all necessary attributes are correctly set before
+ initializing the OpenAI client. It creates an instance of the OpenAI
+ client using the provided API key and base URL, storing it in the
+
+ Returns:
+ self: Returns the instance of the current class for method chaining.
+ """
+ self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
+ return self
+
+ def stream_chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
+ for i in range(self.max_retries):
+ try:
+ completion = self._client.chat.completions.create(
+ model=self.model_name,
+ messages=[x.simple_dict for x in messages],
+ seed=self.seed,
+ top_p=self.top_p,
+ stream=True,
+ stream_options=self.stream_options,
+ temperature=self.temperature,
+ extra_body={"enable_thinking": self.enable_thinking},
+ tools=[x.simple_dict for x in tools] if tools else None,
+ tool_choice=self.tool_choice,
+ parallel_tool_calls=self.parallel_tool_calls)
+
+ ret_tools = []
+ is_answering = False
+
+ for chunk in completion:
+ if not chunk.choices:
+ yield chunk.usage, ChunkEnum.USAGE
+
+ else:
+ delta = chunk.choices[0].delta
+ if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
+ yield delta.reasoning_content, ChunkEnum.THINK
+
+ else:
+ if not is_answering:
+ is_answering = True
+
+ if delta.content is not None:
+ yield delta.content, ChunkEnum.ANSWER
+
+ if delta.tool_calls is not None:
+ for tool_call in delta.tool_calls:
+ index = tool_call.index
+
+ while len(ret_tools) <= index:
+ ret_tools.append(ToolCall(index=index))
+
+ if tool_call.id:
+ ret_tools[index].id += tool_call.id
+
+ if tool_call.function and tool_call.function.name:
+ ret_tools[index].name += tool_call.function.name
+
+ if tool_call.function and tool_call.function.arguments:
+ ret_tools[index].arguments += tool_call.function.arguments
+
+ if ret_tools:
+ tool_dict = {x.name: x for x in tools}
+ for tool in ret_tools:
+ if tool.name not in tool_dict:
+ continue
+
+ yield tool, ChunkEnum.TOOL
+
+ return
+
+ except Exception as e:
+ logger.exception(f"stream chat with model={self.model_name} encounter error with e={e.args}")
+ if i == self.max_retries - 1 and self.raise_exception:
+ raise e
+ else:
+ yield e.args, ChunkEnum.ERROR
+
+ def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage:
+ # usage = None
+ reasoning_content = ""
+ answer_content = ""
+ tool_calls = []
+
+ for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
+ if chunk_enum is ChunkEnum.USAGE:
+ usage = chunk
+
+ elif chunk_enum is ChunkEnum.THINK:
+ reasoning_content += chunk
+
+ elif chunk_enum is ChunkEnum.ANSWER:
+ answer_content += chunk
+
+ elif chunk_enum is ChunkEnum.TOOL:
+ tool_calls.append(chunk)
+
+ return ActionMessage(reasoning_content=reasoning_content,
+ content=answer_content,
+ tool_calls=tool_calls)
+
+ def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
+ enter_think = False
+ enter_answer = False
+ for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
+ if chunk_enum is ChunkEnum.USAGE:
+ if isinstance(chunk, CompletionUsage):
+ print(f"\n{chunk.model_dump_json(indent=2)}")
+ else:
+ print(f"\n{chunk}")
+
+ elif chunk_enum is ChunkEnum.THINK:
+ if not enter_think:
+ enter_think = True
+ print("\n", end="")
+ print(chunk, end="")
+
+ elif chunk_enum is ChunkEnum.ANSWER:
+ if not enter_answer:
+ enter_answer = True
+ if enter_think:
+ print("\n")
+ print(chunk, end="")
+
+ elif chunk_enum is ChunkEnum.TOOL:
+ assert isinstance(chunk, ToolCall)
+ print(f"\n{chunk.model_dump_json(indent=2)}", end="")
+
+ elif chunk_enum is ChunkEnum.ERROR:
+ print(f"\n{chunk}", end="")
diff --git a/experiencescope/module/__init__.py b/experiencescope/module/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/agent_wrapper/__init__.py b/experiencescope/module/agent_wrapper/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/agent_wrapper/base_agent_wrapper.py b/experiencescope/module/agent_wrapper/base_agent_wrapper.py
new file mode 100644
index 00000000..c32b88d2
--- /dev/null
+++ b/experiencescope/module/agent_wrapper/base_agent_wrapper.py
@@ -0,0 +1,91 @@
+from abc import ABC
+from typing import List
+
+from loguru import logger
+from pydantic import Field
+
+from beyondagent.core.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper
+from beyondagent.core.module.environment.base_environment import BaseEnvironment
+from beyondagent.core.schema.trajectory import Trajectory, Message, StateMessage, ActionMessage, ContextMessage
+
+
+class BaseAgentWrapper(BaseAgentWrapperMixin, ABC):
+ max_steps: int = Field(default=10)
+ enable_exploration: bool = Field(default=False)
+ trajectory: Trajectory | None = Field(default_factory=Trajectory)
+
+ def reset(self):
+ self.trajectory = Trajectory()
+
+ def before_execute_hook(self, query: str, **kwargs):
+ self.trajectory.query = query
+
+ def after_step_hook(self, action_msg: ActionMessage, next_state: StateMessage, **kwargs):
+ raise NotImplementedError
+
+ def build_messages(self,
+ state: StateMessage,
+ context_msg: ContextMessage | None,
+ env: BaseEnvironment, **kwargs) -> List[Message]:
+ raise NotImplementedError
+
+ def explore_messages(self, messages: List[Message], **kwargs) -> List[Message]:
+ raise NotImplementedError
+
+ def action_parser(self, action_msg: ActionMessage, **kwargs) -> ActionMessage:
+ return action_msg
+
+ def generate_action(self,
+ state: StateMessage,
+ context_msg: ContextMessage | None,
+ env: BaseEnvironment,
+ **kwargs) -> ActionMessage:
+
+ messages: List[Message] = self.build_messages(state, context_msg, env, **kwargs)
+ if self.enable_exploration:
+ messages = self.explore_messages(messages, **kwargs)
+
+ action_msg: ActionMessage = self.llm.chat(messages, tools=env.tools)
+ return self.action_parser(action_msg)
+
+ def after_execute_hook(self, **kwargs):
+ return self.trajectory
+
+ def execute(self, query: str, env: BaseEnvironment = None, **kwargs) -> Trajectory:
+ self.before_execute_hook(query=query, **kwargs)
+
+ current_state = env.current_state
+ for i in range(self.max_steps):
+ self.trajectory.current_step = i
+
+ # generate context
+ context_msg: ContextMessage | None = None
+ if self.context_generator:
+ context_msg = self.context_generator.execute(trajectory=self.trajectory, **kwargs)
+ if context_msg.content:
+ logger.info(f"step{i}.context_msg={context_msg.content}")
+
+ # generate action
+ action_msg = self.generate_action(state=current_state, context_msg=context_msg, env=env, **kwargs)
+ logger.info(f"step{i} ====== reasoning_content ======\n{action_msg.reasoning_content}\n\n"
+ f"====== content ======\n{action_msg.content}\ntool_calls={action_msg.tool_calls}")
+
+ # generate next state
+ next_state, reward, done, info = env.step(action_msg, trajectory=self.trajectory, **kwargs)
+ logger.info(f"step{i}.next_state={next_state.content} reward={reward.reward_value} done={done} info={info}")
+
+ self.after_step_hook(step_index=i,
+ current_state=current_state,
+ action_msg=action_msg,
+ reward=reward,
+ next_state=next_state,
+ done=done,
+ info=info,
+ **kwargs)
+
+ if done:
+ break
+
+ current_state = next_state
+
+ return self.after_execute_hook(**kwargs)
diff --git a/experiencescope/module/agent_wrapper/base_agent_wrapper_mixin.py b/experiencescope/module/agent_wrapper/base_agent_wrapper_mixin.py
new file mode 100644
index 00000000..e7d4f469
--- /dev/null
+++ b/experiencescope/module/agent_wrapper/base_agent_wrapper_mixin.py
@@ -0,0 +1,27 @@
+from abc import ABC
+
+from pydantic import Field, BaseModel
+
+from beyondagent.core.module.base_module import BaseModule
+from beyondagent.core.module.context_generator.base_context_generator import BaseContextGenerator
+from beyondagent.core.schema.trajectory import Trajectory, ActionMessage, Message
+
+
+class BaseAgentWrapperMixin(BaseModule, ABC):
+ context_generator: BaseContextGenerator | None = Field(default=None)
+
+ def execute(self, query: str, **kwargs) -> Trajectory:
+ raise NotImplementedError
+
+
+class MockAgentWrapper(BaseAgentWrapperMixin):
+
+ def execute(self, query: str, **kwargs) -> Trajectory:
+ user_message = Message(content=query)
+ answer_message = ActionMessage(content="hello world")
+
+ traj = Trajectory(steps=[user_message, answer_message],
+ done=True,
+ query=query,
+ answer=answer_message.content)
+ return traj
diff --git a/experiencescope/module/agent_wrapper/naive_agent_wrapper.py b/experiencescope/module/agent_wrapper/naive_agent_wrapper.py
new file mode 100644
index 00000000..7c28a308
--- /dev/null
+++ b/experiencescope/module/agent_wrapper/naive_agent_wrapper.py
@@ -0,0 +1,62 @@
+import datetime
+
+from loguru import logger
+
+from beyondagent.core.module.agent_wrapper.base_agent_wrapper_v2 import BaseAgentWrapperV2
+from beyondagent.core.module.environment.base_environment import BaseEnvironment
+from beyondagent.core.schema.trajectory import Message, StateMessage, ContextMessage, ActionMessage
+
+
+class NaiveAgentWrapper(BaseAgentWrapperV2):
+
+ def generate_action(self,
+ state: StateMessage,
+ context_msg: ContextMessage | None,
+ env: BaseEnvironment,
+ **kwargs) -> ActionMessage:
+
+ tool_names = [x.name for x in env.tools]
+ if self.trajectory.current_step == 0:
+ now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ insight_tag: bool = True if context_msg is not None and context_msg.content else False
+ user_prompt = self.prompt_handler.prompt_format(
+ prompt_name="role_prompt",
+ insight_tag=insight_tag,
+ time=now_time,
+ tools=", ".join(tool_names),
+ previous_insight=context_msg.content if insight_tag else "",
+ query=self.trajectory.query)
+ # When using the reasoning models of Qwen3 or DeepSeek R1, it is not recommended to use system prompt.
+ self.trajectory.steps.append(Message(content=user_prompt))
+
+ elif self.trajectory.metadata.get("has_terminate_tool") is True:
+ user_prompt = self.prompt_handler.final_prompt.format(query=self.trajectory.query)
+ self.trajectory.steps.append(Message(content=user_prompt))
+
+ else:
+ user_prompt = self.prompt_handler.next_prompt.format(query=self.trajectory.query)
+ self.trajectory.steps.append(Message(content=user_prompt))
+
+ if self.trajectory.metadata.get("has_terminate_tool") is True:
+ action_msg: ActionMessage = self.llm.chat(self.trajectory.steps)
+ logger.info(f"step{self.trajectory.current_step} size={len(self.trajectory.steps)} user_prompt={user_prompt}")
+
+ else:
+ action_msg: ActionMessage = self.llm.chat(messages, tools=env.tools)
+ logger.info(f"step{self.trajectory.current_step} size={len(messages)} user_prompt={user_prompt} "
+ f"tool_names={tool_names}")
+
+ for tool in action_msg.tool_calls:
+ if tool.name == "terminate":
+ self.trajectory.metadata["has_terminate_tool"] = True
+ break
+
+ self.trajectory.add_step(action_msg)
+ return action_msg
+
+
+ def after_step_hook(self, action_msg: ActionMessage, next_state: StateMessage, done: bool = False, **kwargs):
+ if done:
+ self.trajectory.answer = action_msg.content
+ else:
+ self.trajectory.add_step(next_state)
diff --git a/experiencescope/module/base_module.py b/experiencescope/module/base_module.py
new file mode 100644
index 00000000..7616076b
--- /dev/null
+++ b/experiencescope/module/base_module.py
@@ -0,0 +1,48 @@
+from abc import ABC
+
+from loguru import logger
+from pydantic import BaseModel, Field, model_validator
+
+from beyondagent.core.model import LLM_REGISTRY, EMBEDDING_MODEL_REGISTRY
+from beyondagent.core.model.base_embedding_model import BaseEmbeddingModel
+from beyondagent.core.model.base_llm import BaseLLM
+from beyondagent.core.utils.prompt_handler import PromptHandler
+
+
+class BaseModule(BaseModel, ABC):
+ prompt_dir: str | None = Field(default=None)
+ prompt_file: str | None = Field(default=None)
+ prompt_handler: PromptHandler | None = Field(default=None)
+
+ llm: BaseLLM | None = Field(default=None)
+ embedding_model: BaseEmbeddingModel | None = Field(default=None)
+
+ @model_validator(mode="before") # noqa
+ @classmethod
+ def init_model(cls, data: dict):
+ if "llm" in data and isinstance(data["llm"], dict):
+ backend = data["llm"].pop("backend", None)
+ assert backend is not None, "llm must have a backend"
+ module = LLM_REGISTRY[backend]
+ params = data["llm"]
+ data["llm"] = module(**params)
+ logger.info(f"{cls.__name__} load llm.backend={backend} params={params}")
+
+ if "embedding_model" in data and isinstance(data["embedding_model"], dict):
+ backend = data["embedding_model"].pop("backend", None)
+ assert backend is not None, "embedding_model must have a backend"
+ module = EMBEDDING_MODEL_REGISTRY[backend]
+ params = data["embedding_model"]
+ data["embedding_model"] = module(**params)
+ logger.info(f"{cls.__name__} load embedding_model.backend={backend} params={params}")
+
+ if "prompt_dir" in data:
+ handler = PromptHandler(dir_path=data.get("prompt_dir"))
+ data["prompt_handler"] = handler
+ if data.get("prompt_file"):
+ handler.add_prompt_file(data.get("prompt_file"))
+
+ return data
+
+ def execute(self, **kwargs):
+ raise NotImplementedError
diff --git a/experiencescope/module/context_generator/__init__.py b/experiencescope/module/context_generator/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/context_generator/base_context_generator.py b/experiencescope/module/context_generator/base_context_generator.py
new file mode 100644
index 00000000..da8ac911
--- /dev/null
+++ b/experiencescope/module/context_generator/base_context_generator.py
@@ -0,0 +1,37 @@
+from abc import ABC
+from typing import List
+
+from pydantic import Field
+
+from beyondagent.core.module.base_module import BaseModule
+from beyondagent.core.schema.trajectory import Trajectory, ContextMessage
+from beyondagent.core.schema.vector_store_node import VectorStoreNode
+from beyondagent.core.storage.base_vector_store import BaseVectorStore
+
+
+class BaseContextGenerator(BaseModule, ABC):
+ vector_store: BaseVectorStore | None = Field(default=None)
+
+ def _build_retrieve_query(self, trajectory: Trajectory, **kwargs) -> str:
+ raise NotImplementedError
+
+ def _retrieve_by_query(self, trajectory: Trajectory, query: str, **kwargs) -> List[VectorStoreNode]:
+ raise NotImplementedError
+
+ def _generate_context_message(self,
+ trajectory: Trajectory,
+ nodes: List[VectorStoreNode],
+ **kwargs) -> ContextMessage:
+ raise NotImplementedError
+
+ def execute(self, trajectory: Trajectory, **kwargs) -> ContextMessage:
+ query: str = self._build_retrieve_query(trajectory, **kwargs)
+ nodes: List[VectorStoreNode] = self._retrieve_by_query(trajectory, query, **kwargs)
+ context_msg: ContextMessage = self._generate_context_message(trajectory, nodes, **kwargs)
+ return context_msg
+
+
+class MockContextGenerator(BaseContextGenerator):
+
+ def execute(self, trajectory: Trajectory, **kwargs) -> ContextMessage:
+ return ContextMessage(content="mock context")
diff --git a/experiencescope/module/environment/__init__.py b/experiencescope/module/environment/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/environment/base_environment.py b/experiencescope/module/environment/base_environment.py
new file mode 100644
index 00000000..a08875ad
--- /dev/null
+++ b/experiencescope/module/environment/base_environment.py
@@ -0,0 +1,68 @@
+from typing import List
+
+from pydantic import Field
+
+from beyondagent.core.module.base_module import BaseModule
+from beyondagent.core.module.reward_fn.base_reward_fn import BaseRewardFn
+from beyondagent.core.schema.reward import Reward
+from beyondagent.core.schema.trajectory import StateMessage, ActionMessage, ToolCall
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class BaseEnvironment(BaseModule):
+ tools: List[BaseTool] = Field(default_factory=list)
+ reward_fns: List[BaseRewardFn] = Field(default_factory=list)
+ current_state: StateMessage | None = Field(default=None)
+ metadata: dict = Field(default_factory=dict, description="add query / answer and etc for reward calculating!")
+
+ def reset(self):
+ self.current_state = StateMessage()
+ self.metadata.clear()
+
+ def step(self, action_msg: ActionMessage, **kwargs):
+ next_state: StateMessage = self.transition(action_msg=action_msg, **kwargs)
+
+ reward: Reward = self.calculate_reward(action_msg=action_msg, next_state=next_state, **kwargs)
+
+ done: bool = self.is_terminated(action_msg=action_msg, next_state=next_state, reward=reward, **kwargs)
+
+ info: dict = self.build_info(action_msg=action_msg, next_state=next_state, reward=reward, done=done, **kwargs)
+
+ self.current_state = next_state
+ return next_state, reward, done, info
+
+ def transition(self, action_msg: ActionMessage, **kwargs) -> StateMessage:
+ tool_dict = {tool.name: tool for tool in self.tools}
+
+ new_tool_calls: List[ToolCall] = []
+ for tool_call in action_msg.tool_calls:
+ if tool_call.name not in tool_dict:
+ continue
+
+ new_tool_call = tool_call.model_copy(deep=True)
+ tool = tool_dict[tool_call.name]
+ new_tool_call.result = tool.execute(**tool_call.argument_dict)
+ new_tool_calls.append(new_tool_call)
+
+ return StateMessage(tool_calls=new_tool_calls)
+
+ def calculate_reward(self, **kwargs) -> Reward:
+ return Reward()
+
+ def is_terminated(self, **kwargs) -> bool:
+ raise NotImplementedError
+
+ def build_info(self, **kwargs):
+ return {}
+
+ def get_tool_info(self,tool_name):
+ tool_dict = {tool.name: tool for tool in self.tools}
+ if tool_name in tool_dict:
+
+ return f'tool \'{tool_name}\' description is: {tool_dict[tool_name].description}\t' + f'parameters: {str(tool_dict[tool_name].input_schema)}'
+ else:
+ return ''
+
+ def get_tools_info(self):
+ tool_dict = {tool.name: tool for tool in self.tools}
+ return {tool_name:self.get_tool_info(tool_name=tool_name) for tool_name in tool_dict}
\ No newline at end of file
diff --git a/experiencescope/module/evaluator/__init__.py b/experiencescope/module/evaluator/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/evaluator/base_evaluator.py b/experiencescope/module/evaluator/base_evaluator.py
new file mode 100644
index 00000000..1ae5b515
--- /dev/null
+++ b/experiencescope/module/evaluator/base_evaluator.py
@@ -0,0 +1,19 @@
+from abc import ABC
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper
+from beyondagent.core.module.context_generator.base_context_generator import BaseContextGenerator
+from beyondagent.core.module.environment.base_environment import BaseEnvironment
+from beyondagent.core.module.summarizer.base_summarizer import BaseSummarizer
+
+
+class BaseEvaluator(BaseModel, ABC):
+ data_path: str = Field(default="")
+ agent_wrapper: BaseAgentWrapper | None = Field(default=None)
+ context_generator: BaseContextGenerator | None = Field(default=None)
+ summarizer: BaseSummarizer | None = Field(default=None)
+ env: BaseEnvironment | None = Field(default=None)
+
+ def evaluate(self, **kwargs):
+ raise NotImplementedError
diff --git a/experiencescope/module/reward_fn/__init__.py b/experiencescope/module/reward_fn/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/reward_fn/base_reward_fn.py b/experiencescope/module/reward_fn/base_reward_fn.py
new file mode 100644
index 00000000..5f84b738
--- /dev/null
+++ b/experiencescope/module/reward_fn/base_reward_fn.py
@@ -0,0 +1,11 @@
+from abc import ABC
+
+from beyondagent.core.module.base_module import BaseModule
+from beyondagent.core.schema.reward import Reward
+from beyondagent.core.schema.trajectory import Trajectory
+
+
+class BaseRewardFn(BaseModule, ABC):
+
+ def execute(self, trajectory: Trajectory, ground_truth=None, **kwargs) -> Reward:
+ raise NotImplementedError
diff --git a/experiencescope/module/runner/__init__.py b/experiencescope/module/runner/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/runner/base_runner.py b/experiencescope/module/runner/base_runner.py
new file mode 100644
index 00000000..7967466a
--- /dev/null
+++ b/experiencescope/module/runner/base_runner.py
@@ -0,0 +1,29 @@
+from typing import List
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper
+from beyondagent.core.module.context_generator.base_context_generator import BaseContextGenerator
+from beyondagent.core.module.environment.base_environment import BaseEnvironment
+from beyondagent.core.module.summarizer.base_summarizer import BaseSummarizer
+from beyondagent.core.schema.trajectory import Trajectory
+
+
+class BaseRunner(BaseModel):
+ agent_wrapper: BaseAgentWrapper | None = Field(default=None)
+ context_generator: BaseContextGenerator | None = Field(default=None)
+ summarizer: BaseSummarizer | None = Field(default=None)
+ env: BaseEnvironment | None = Field(default=None)
+ traj_buffer: List[Trajectory] = Field(default_factory=list)
+
+ def reset(self):
+ self.traj_buffer.clear()
+
+ def rollout_trajectory(self, user_query: str, **kwargs):
+ raise NotImplementedError
+
+ def summary(self):
+ raise NotImplementedError
+
+ def start_backend_summary(self):
+ raise NotImplementedError
diff --git a/experiencescope/module/summarizer/__init__.py b/experiencescope/module/summarizer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/summarizer/base_summarizer.py b/experiencescope/module/summarizer/base_summarizer.py
new file mode 100644
index 00000000..1505e846
--- /dev/null
+++ b/experiencescope/module/summarizer/base_summarizer.py
@@ -0,0 +1,37 @@
+from typing import List
+
+from pydantic import Field
+
+from beyondagent.core.module.base_module import BaseModule
+from beyondagent.core.schema.trajectory import Trajectory, Sample, SummaryMessage
+from beyondagent.core.storage.base_vector_store import BaseVectorStore
+
+
+class BaseSummarizer(BaseModule):
+ vector_store: BaseVectorStore | None = Field(default=None)
+
+ def extract_samples(self, trajectories: List[Trajectory], **kwargs) -> List[Sample]:
+ raise NotImplementedError
+
+ def insert_into_vector_store(self, samples: List[Sample], **kwargs):
+ raise NotImplementedError
+
+ def execute(self, trajectories: List[Trajectory], return_samples: bool = True, **kwargs) -> List[Sample]:
+ samples: List[Sample] = self.extract_samples(trajectories, **kwargs)
+ self.insert_into_vector_store(samples, **kwargs)
+
+ if return_samples:
+ return samples
+
+ return []
+
+
+class MockSummarizer(BaseSummarizer):
+
+ def execute(self, trajectories: List[Trajectory], return_samples: bool = True, **kwargs) -> List[Sample]:
+ tip_message = SummaryMessage(content="I am a mock summarizer.")
+
+ if return_samples:
+ return [Sample(steps=[tip_message])]
+
+ return []
diff --git a/experiencescope/module/trainner/__init__.py b/experiencescope/module/trainner/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/module/trainner/base_trainner.py b/experiencescope/module/trainner/base_trainner.py
new file mode 100644
index 00000000..4fd4a2af
--- /dev/null
+++ b/experiencescope/module/trainner/base_trainner.py
@@ -0,0 +1,45 @@
+from abc import ABC
+from typing import List
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.schema.trajectory import Trajectory
+
+
+class BaseTrainner(BaseModel, ABC):
+ """
+ load model/prompt
+ data
+ env
+ -> 新cpt
+
+ off policy/onpolicy
+ """
+ traj_buffer: List[Trajectory] = Field()
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ def fit(self):
+ return
+
+ def save_module_state(self):
+ return
+
+
+class BaseContextTrainner(BaseTrainner):
+ """
+ load model/prompt/db/buffer -> 新cpt
+ """
+
+
+class BaseSummaryTrainner(BaseTrainner):
+ """
+ load model/prompt/db/buffer -> 新cpt
+ """
+
+
+class BasePolicyTrainner(BaseTrainner):
+ """
+ load model/prompt/db/buffer -> 新cpt
+ """
diff --git a/experiencescope/schema/__init__.py b/experiencescope/schema/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/schema/module_loader.py b/experiencescope/schema/module_loader.py
new file mode 100644
index 00000000..4dde4a2b
--- /dev/null
+++ b/experiencescope/schema/module_loader.py
@@ -0,0 +1,19 @@
+from importlib import import_module
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.utils.file_handler import FileHandler
+
+
+class ModuleLoader(BaseModel):
+ class_path: str = Field(default=...)
+ class_name: str = Field(default=...)
+ config_path: str = Field(default="")
+ config: dict = Field(default_factory=dict)
+
+ def load_from_config(self):
+ return getattr(import_module(self.class_path), self.class_name)(**self.config)
+
+ def load_from_path(self):
+ self.config = FileHandler(file_path=self.config_path).load()
+ return self.load_from_config()
diff --git a/experiencescope/schema/request.py b/experiencescope/schema/request.py
new file mode 100644
index 00000000..21a42656
--- /dev/null
+++ b/experiencescope/schema/request.py
@@ -0,0 +1,24 @@
+from abc import ABC
+from typing import List
+
+from pydantic import Field
+
+from beyondagent.core.schema.module_loader import ModuleLoader
+from beyondagent.core.schema.trajectory import Trajectory
+
+
+class BaseRequest(ModuleLoader, ABC):
+ metadata: dict = Field(default_factory=dict)
+
+
+class AgentWrapperRequest(BaseRequest):
+ query: str = Field(default="")
+
+
+class ContextGeneratorRequest(BaseRequest):
+ trajectory: Trajectory = Field(default_factory=Trajectory)
+
+
+class SummarizerRequest(BaseRequest):
+ trajectories: List[Trajectory] = Field(default_factory=dict)
+ return_samples: bool = Field(default=False)
diff --git a/experiencescope/schema/response.py b/experiencescope/schema/response.py
new file mode 100644
index 00000000..db3c9ae8
--- /dev/null
+++ b/experiencescope/schema/response.py
@@ -0,0 +1,23 @@
+from abc import ABC
+from typing import List
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.schema.trajectory import Trajectory, ContextMessage, Sample
+
+
+class BaseResponse(BaseModel, ABC):
+ success: bool = Field(default=True)
+ metadata: dict = Field(default_factory=dict)
+
+
+class AgentWrapperResponse(BaseResponse):
+ trajectory: Trajectory = Field(default_factory=Trajectory)
+
+
+class ContextGeneratorResponse(BaseResponse):
+ context_msg: ContextMessage = Field(default_factory=ContextMessage)
+
+
+class SummarizerResponse(BaseResponse):
+ extract_samples: List[Sample] = Field(default_factory=list)
diff --git a/experiencescope/schema/reward.py b/experiencescope/schema/reward.py
new file mode 100644
index 00000000..8636e3b6
--- /dev/null
+++ b/experiencescope/schema/reward.py
@@ -0,0 +1,6 @@
+from pydantic import Field, BaseModel
+
+
+class Reward(BaseModel):
+ reward_value: float | None = Field(default=None)
+ metadata: dict = Field(default_factory=dict)
diff --git a/experiencescope/schema/trajectory.py b/experiencescope/schema/trajectory.py
new file mode 100644
index 00000000..46e93336
--- /dev/null
+++ b/experiencescope/schema/trajectory.py
@@ -0,0 +1,127 @@
+import datetime
+import json
+from typing import List, Any
+from uuid import uuid4
+
+from pydantic import BaseModel, Field, model_validator
+
+from beyondagent.core.enumeration.role import Role
+
+
+class ToolCall(BaseModel):
+ index: int = Field(default=...)
+ id: str = Field(default="")
+ name: str = Field(default="")
+ arguments: str = Field(default="")
+ type: str = Field(default="function")
+ result: Any = Field(default=None, exclude=True)
+
+ @model_validator(mode="before") # noqa
+ @classmethod
+ def init_tool_call(cls, data: dict):
+ tool_type = data.get("type", "")
+ tool_type_dict = data.get(tool_type, {})
+
+ for key in ["name", "arguments"]:
+ if key not in data:
+ data[key] = tool_type_dict.get(key, "")
+ return data
+
+ @property
+ def argument_dict(self):
+ return json.loads(self.arguments)
+
+ @property
+ def simple_dict(self):
+ return {
+ "id": self.id,
+ self.type: {
+ "arguments": self.arguments,
+ "name": self.name
+ },
+ "type": self.type,
+ "index": self.index,
+ }
+
+
+class Message(BaseModel):
+ role: Role = Field(default=Role.USER)
+ content: str | bytes = Field(default="")
+ reasoning_content: str = Field(default="")
+ tool_calls: List[ToolCall] = Field(default_factory=list)
+ timestamp: str = Field(
+ default_factory=lambda: datetime.datetime.now().strftime(
+ "%Y-%m-%d %H:%M:%S.%f",
+ ),
+ )
+ metadata: dict = Field(default_factory=dict)
+
+ @property
+ def simple_dict(self) -> dict:
+ result = {
+ "role": self.role.value,
+ "content": self.content,
+ }
+ if self.tool_calls:
+ result["tool_calls"] = [x.simple_dict for x in self.tool_calls]
+ return result
+
+
+class ActionMessage(Message):
+ role: Role = Field(default=Role.ASSISTANT)
+
+
+class StateMessage(Message):
+ role: Role = Field(default=Role.TOOL)
+ tool_call_id: str = Field(default="")
+
+ @property
+ def simple_dict(self) -> dict:
+ result = {
+ "role": self.role.value,
+ "content": self.content,
+ }
+ if self.tool_call_id:
+ result["tool_call_id"] = self.tool_call_id
+ return result
+
+ @property
+ def simple_list(self) -> list:
+ return [{
+ "role": self.role.value,
+ "content": str(x.result),
+ "tool_call_id": x.id,
+ } for x in self.tool_calls]
+
+
+class ContextMessage(Message):
+ role: Role = Field(default=Role.CONTEXT_ASSISTANT)
+
+
+class SummaryMessage(Message):
+ role: Role = Field(default=Role.SUMMARY_ASSISTANT)
+
+
+class Sample(BaseModel):
+ steps: List[Message] = Field(default_factory=list)
+ metadata: dict = Field(default_factory=dict)
+
+
+class Trajectory(BaseModel):
+ id: str = Field(default_factory=lambda: uuid4().hex)
+ steps: List[Message] = Field(default_factory=list)
+
+ done: bool = Field(default=False)
+ query: str = Field(default="")
+ answer: Any = Field(default=None)
+ metadata: dict = Field(default_factory=dict)
+
+ def add_step(self, step: Message):
+ self.steps.append(step)
+
+ def reset(self):
+ self.steps.clear()
+ self.done = False
+ self.query = ""
+ self.answer = ""
+ self.metadata.clear()
diff --git a/experiencescope/schema/vector_store_node.py b/experiencescope/schema/vector_store_node.py
new file mode 100644
index 00000000..55ede7e9
--- /dev/null
+++ b/experiencescope/schema/vector_store_node.py
@@ -0,0 +1,12 @@
+from typing import List
+from uuid import uuid4
+
+from pydantic import BaseModel, Field
+
+
+class VectorStoreNode(BaseModel):
+ unique_id: str = Field(default_factory=lambda: uuid4().hex)
+ workspace_id: str = Field(default="")
+ content: str = Field(default="")
+ vector: List[float] | None = Field(default=None)
+ metadata: dict = Field(default_factory=dict)
diff --git a/experiencescope/service/__init__.py b/experiencescope/service/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/service/model_service.py b/experiencescope/service/model_service.py
new file mode 100644
index 00000000..4ff0e94e
--- /dev/null
+++ b/experiencescope/service/model_service.py
@@ -0,0 +1,42 @@
+from beyondagent.core.utils.logger import init_logger
+init_logger()
+
+from typing import List
+from fastapi import FastAPI
+from beyondagent.core.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper
+from beyondagent.core.module.context_generator.base_context_generator import BaseContextGenerator
+from beyondagent.core.module.summarizer.base_summarizer import BaseSummarizer
+from beyondagent.core.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest
+from beyondagent.core.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse
+from beyondagent.core.schema.trajectory import ContextMessage, Trajectory, Sample
+import uvicorn
+
+app = FastAPI()
+
+
+@app.post('/agent_wrapper', response_model=AgentWrapperResponse)
+def call_agent_wrapper(request: AgentWrapperRequest):
+ module: BaseAgentWrapper = request.load_from_path()
+ trajectory: Trajectory = module.execute(request.query, **request.metadata)
+ return AgentWrapperResponse(trajectory=trajectory)
+
+
+@app.post('/context_generator', response_model=ContextGeneratorResponse)
+def call_context_generator(request: ContextGeneratorRequest):
+ module: BaseContextGenerator = request.load_from_path()
+ context_msg: ContextMessage = module.execute(request.trajectory, **request.metadata)
+ return ContextGeneratorResponse(context_msg=context_msg)
+
+
+@app.post('/summarizer', response_model=SummarizerResponse)
+def call_summarizer(request: SummarizerRequest):
+ module: BaseSummarizer = request.load_from_path()
+ samples: List[Sample] = module.execute(request.trajectories, request.return_samples, **request.metadata)
+ return SummarizerResponse(extract_samples=samples)
+
+
+if __name__ == '__main__':
+ uvicorn.run(app, host="0.0.0.0", port=8000, timeout_keep_alive=600000, limit_concurrency=32)
+
+# launch with:
+# python -m beyondagent.core.service.model_service
\ No newline at end of file
diff --git a/experiencescope/service/model_service_client.py b/experiencescope/service/model_service_client.py
new file mode 100644
index 00000000..5b1f3c45
--- /dev/null
+++ b/experiencescope/service/model_service_client.py
@@ -0,0 +1,24 @@
+from pydantic import Field
+
+from beyondagent.core.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest
+from beyondagent.core.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse
+from beyondagent.core.utils.http_client import HttpClient
+
+
+class ModelServiceClient(HttpClient):
+ base_url: str = Field(default=...)
+
+ def call_agent_wrapper(self, request: AgentWrapperRequest):
+ self.url = self.base_url + "/agent_wrapper"
+ return AgentWrapperResponse(**self.request(json_data=request.model_dump(),
+ headers={"Content-Type": "application/json"}))
+
+ def call_context_generator(self, request: ContextGeneratorRequest):
+ self.url = self.base_url + "/context_generator"
+ return ContextGeneratorResponse(**self.request(json_data=request.model_dump(),
+ headers={"Content-Type": "application/json"}))
+
+ def call_summarizer(self, request: SummarizerRequest):
+ self.url = self.base_url + "/summarizer"
+ return SummarizerResponse(**self.request(json_data=request.model_dump(),
+ headers={"Content-Type": "application/json"}))
diff --git a/experiencescope/storage/__init__.py b/experiencescope/storage/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/storage/base_sample_buffer.py b/experiencescope/storage/base_sample_buffer.py
new file mode 100644
index 00000000..969e68f1
--- /dev/null
+++ b/experiencescope/storage/base_sample_buffer.py
@@ -0,0 +1,17 @@
+from typing import List
+
+from pydantic import BaseModel
+
+from beyondagent.core.schema.trajectory import Sample
+
+
+class BaseSampleBuffer(BaseModel):
+
+ def add(self, samples: Sample | List[Sample]):
+ raise NotImplementedError
+
+ def get_all(self):
+ raise NotImplementedError
+
+ def clear(self):
+ raise NotImplementedError
diff --git a/experiencescope/storage/base_vector_store.py b/experiencescope/storage/base_vector_store.py
new file mode 100644
index 00000000..f49100b9
--- /dev/null
+++ b/experiencescope/storage/base_vector_store.py
@@ -0,0 +1,26 @@
+from abc import ABC
+from typing import List
+
+from pydantic import BaseModel, Field
+
+from beyondagent.core.model.base_embedding_model import BaseEmbeddingModel
+from beyondagent.core.schema.vector_store_node import VectorStoreNode
+
+
+class BaseVectorStore(BaseModel, ABC):
+ embedding_model: BaseEmbeddingModel = Field(default=...)
+
+ def insert(self, nodes: VectorStoreNode | List[VectorStoreNode], **kwargs):
+ raise NotImplementedError
+
+ def update(self, nodes: VectorStoreNode | List[VectorStoreNode], **kwargs):
+ raise NotImplementedError
+
+ def delete_by_id(self, unique_id: str, **kwargs):
+ raise NotImplementedError
+
+ def retrieve_by_id(self, unique_id: str, **kwargs) -> VectorStoreNode | None:
+ raise NotImplementedError
+
+ def retrieve_by_query(self, query: str, top_k: int = 3, **kwargs) -> List[VectorStoreNode]:
+ raise NotImplementedError
diff --git a/experiencescope/storage/es_vector_store.py b/experiencescope/storage/es_vector_store.py
new file mode 100644
index 00000000..312bfe5d
--- /dev/null
+++ b/experiencescope/storage/es_vector_store.py
@@ -0,0 +1,169 @@
+import os
+from typing import List, Tuple
+
+from elasticsearch import Elasticsearch
+from elasticsearch.helpers import bulk
+from loguru import logger
+from pydantic import Field, PrivateAttr, model_validator
+
+from beyondagent.core.schema.vector_store_node import VectorStoreNode
+from beyondagent.core.storage.base_vector_store import BaseVectorStore
+
+
+class EsVectorStore(BaseVectorStore):
+ hosts: str | List[str] = Field(default_factory=lambda: os.getenv("ES_HOSTS", "http://localhost:9200"))
+ index_name: str = Field(default=...)
+ basic_auth: str | Tuple[str, str] | None = Field(default=None)
+ bulk_chunk_size: int = Field(default=512)
+ retrieve_filters: List[dict] = []
+ _client: Elasticsearch = PrivateAttr()
+
+ @model_validator(mode="after")
+ def init_client(self):
+ if isinstance(self.hosts, str):
+ hosts = [self.hosts]
+ else:
+ hosts = self.hosts
+ self._client = Elasticsearch(hosts=hosts, basic_auth=self.basic_auth)
+ return self
+
+ def delete_index(self):
+ if self._client.indices.exists(index=self.index_name):
+ self._client.indices.delete(index=self.index_name)
+
+ def create_index(self):
+ if self._client.indices.exists(index=self.index_name):
+ logger.warning(f"index_name={self.index_name} is already exists!")
+ return None
+
+ index = {
+ "mappings": {
+ "properties": {
+ "workspace_id": {"type": "keyword"},
+ "content": {"type": "text"},
+ "metadata": {"type": "object"},
+ "vector": {
+ "type": "dense_vector",
+ "dims": self.embedding_model.dimensions
+ }
+ }
+ }
+ }
+
+ return self._client.indices.create(index=self.index_name, body=index)
+
+ def refresh_index(self):
+ self._client.indices.refresh(index=self.index_name)
+
+ @staticmethod
+ def doc2node(doc) -> VectorStoreNode:
+ node = VectorStoreNode(**doc["_source"])
+ node.unique_id = doc["_id"]
+ if "_score" in doc:
+ node.metadata["score"] = doc["_score"] - 1
+ return node
+
+ def exist_id(self, doc_id: str):
+ return self._client.exists(index=self.index_name, id=doc_id)
+
+ def node2doc(self, node: VectorStoreNode, add_op_type: bool = False) -> dict:
+ doc: dict = {
+ "_index": self.index_name,
+ "_id": node.unique_id,
+ "_source": {
+ "workspace_id": node.workspace_id,
+ "content": node.content,
+ "metadata": node.metadata,
+ "vector": node.vector
+ }
+ }
+
+ if add_op_type:
+ doc["_op_type"] = "update" if self.exist_id(node.unique_id) else "index",
+ return doc
+
+ def add_term_filter(self, key: str, value):
+ if key:
+ self.retrieve_filters.append({"term": {key: value}})
+
+ return self
+
+ def add_range_filter(self, key: str, gte=None, lte=None):
+ if key:
+ if gte is not None and lte is not None:
+ self.retrieve_filters.append({"range": {key: {"gte": gte, "lte": lte}}})
+ elif gte is not None:
+ self.retrieve_filters.append({"range": {key: {"gte": gte}}})
+ elif lte is not None:
+ self.retrieve_filters.append({"range": {key: {"lte": lte}}})
+
+ return self
+
+ def clear_filter(self):
+ self.retrieve_filters.clear()
+ return self
+
+ def insert(self, nodes: VectorStoreNode | List[VectorStoreNode], refresh_index: bool = False, **kwargs):
+ if isinstance(nodes, VectorStoreNode):
+ nodes = [nodes]
+
+ embedded_nodes = [node for node in nodes if node.vector]
+ not_embedded_nodes = [node for node in nodes if not node.vector]
+ now_embedded_nodes = self.embedding_model.get_node_embeddings(not_embedded_nodes)
+
+ docs = [self.node2doc(node, False) for node in embedded_nodes + now_embedded_nodes]
+ status, error = bulk(self._client, docs, chunk_size=self.bulk_chunk_size, **kwargs)
+ logger.info(f"insert sample.size={len(nodes)} status={status} error={error}")
+
+ if refresh_index:
+ self.refresh_index()
+
+ def update(self, nodes: VectorStoreNode | List[VectorStoreNode], refresh_index: bool = False, **kwargs):
+ if isinstance(nodes, VectorStoreNode):
+ nodes = [nodes]
+
+ nodes = self.embedding_model.get_node_embeddings(nodes)
+ docs = [self.node2doc(node, True) for node in nodes]
+ status, error = bulk(self._client, docs, chunk_size=self.bulk_chunk_size, **kwargs)
+ update_size = sum([1 if doc["_op_type"] == "update" else 0 for doc in docs])
+ insert_size = len(docs) - update_size
+ logger.info(f"update update_size={update_size} insert_size={insert_size} status={status} error={error}")
+
+ if refresh_index:
+ self.refresh_index()
+
+ def delete_by_id(self, unique_id: str, **kwargs):
+ return self._client.delete(index=self.index_name, id=unique_id, **kwargs)
+
+ def retrieve_by_id(self, unique_id: str, **kwargs) -> VectorStoreNode | None:
+ try:
+ doc = self._client.get(index=self.index_name, id=unique_id, **kwargs)
+ return self.doc2node(doc)
+
+ except Exception as e:
+ logger.warning(f"retrieve_by_id unique_id={unique_id} is not found with error={e.args}")
+ return None
+
+ def retrieve_by_query(self, query: str, top_k: int = 3, **kwargs) -> List[VectorStoreNode]:
+ query_vector = self.embedding_model.get_embeddings(query)
+
+ body = {
+ "query": {
+ "script_score": {
+ "query": {"bool": {"must": self.retrieve_filters}},
+ "script": {
+ "source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
+ "params": {"query_vector": query_vector},
+ }
+ }
+ },
+ "size": top_k
+ }
+ response = self._client.search(index=self.index_name, body=body, **kwargs)
+
+ nodes: List[VectorStoreNode] = []
+ for doc in response['hits']['hits']:
+ nodes.append(self.doc2node(doc))
+
+ self.retrieve_filters.clear()
+ return nodes
diff --git a/experiencescope/storage/file_vector_store.py b/experiencescope/storage/file_vector_store.py
new file mode 100644
index 00000000..4041ef4b
--- /dev/null
+++ b/experiencescope/storage/file_vector_store.py
@@ -0,0 +1,132 @@
+import json
+import math
+import threading
+from pathlib import Path
+from typing import List, Any
+
+from loguru import logger
+from pydantic import Field, model_validator, PrivateAttr
+
+from beyondagent.core.schema.vector_store_node import VectorStoreNode
+from beyondagent.core.storage.base_vector_store import BaseVectorStore
+
+
+class FileVectorStore(BaseVectorStore):
+ store_dir: str = Field(default="./")
+ index_name: str = Field(default=...)
+ index_path: Path | None = Field(default=None)
+ _thread_lock: Any = PrivateAttr()
+
+ @model_validator(mode="after")
+ def init_client(self):
+ self._thread_lock = threading.Lock()
+
+ store_path = Path(self.store_dir)
+ store_path.mkdir(parents=True, exist_ok=True)
+ self.index_path = store_path / f"{self.index_name}.jsonl"
+ if not self.index_path.exists():
+ self.index_path.touch(exist_ok=True)
+ return self
+
+ def delete_index(self):
+ with self._thread_lock:
+ if self.index_path.exists() and self.index_path.is_file():
+ self.index_path.unlink()
+
+ def create_index(self):
+ with self._thread_lock:
+ if not self.index_path.exists():
+ self.index_path.touch(exist_ok=True)
+
+ def load(self) -> List[VectorStoreNode]:
+ with self._thread_lock:
+ nodes = []
+ with open(self.index_path) as f:
+ for line in f:
+ if line.strip():
+ nodes.append(VectorStoreNode(**json.loads(line)))
+ return nodes
+
+ def _load(self) -> List[VectorStoreNode]:
+ with self._thread_lock:
+ nodes = []
+ with open(self.index_path) as f:
+ for line in f:
+ if line.strip():
+ nodes.append(VectorStoreNode(**json.loads(line)))
+ return nodes
+
+ def _dump(self, nodes: List[VectorStoreNode]):
+ with self._thread_lock:
+ with open(self.index_path, "w") as f:
+ for doc in nodes:
+ f.write(doc.model_dump_json() + "\n")
+
+ def exist_id(self, unique_id: str):
+ nodes = self._load()
+ for node in nodes:
+ if node.unique_id == unique_id:
+ return True
+ return False
+
+ def insert(self, nodes: VectorStoreNode | List[VectorStoreNode], **kwargs):
+ return self.update(nodes, **kwargs)
+
+ def update(self, nodes: VectorStoreNode | List[VectorStoreNode], **kwargs):
+ if isinstance(nodes, VectorStoreNode):
+ nodes = [nodes]
+
+ all_node_dict = {}
+ nodes: List[VectorStoreNode] = self.embedding_model.get_node_embeddings(nodes)
+ exist_nodes: List[VectorStoreNode] = self._load()
+ for node in exist_nodes:
+ all_node_dict[node.unique_id] = node
+
+ update_cnt = 0
+ for node in nodes:
+ if node.unique_id in all_node_dict:
+ update_cnt += 1
+
+ all_node_dict[node.unique_id] = node
+
+ self._dump(list(all_node_dict.values()))
+ logger.info(f"update nodes.size={len(nodes)} all.size={len(all_node_dict)} update_cnt={update_cnt}")
+
+ def delete_by_id(self, unique_id: str, **kwargs):
+ nodes = self._load()
+ dump_nodes: List[VectorStoreNode] = []
+ for node in nodes:
+ if node.unique_id != unique_id:
+ dump_nodes.append(node)
+
+ if len(dump_nodes) < len(nodes):
+ self._dump(dump_nodes)
+ logger.info(f"delete_by_id unique_id={unique_id}")
+
+ def retrieve_by_id(self, unique_id: str, **kwargs) -> VectorStoreNode | None:
+ nodes = self._load()
+ for node in nodes:
+ if node.unique_id == unique_id:
+ return node
+ return None
+
+ @staticmethod
+ def calculate_similarity(query_vector: List[float], node_vector: List[float]):
+ assert query_vector, f"query_vector is empty!"
+ assert node_vector, f"node_vector is empty!"
+ assert len(query_vector) == len(node_vector), \
+ f"query_vector.size={len(query_vector)} node_vector.size={len(node_vector)}"
+
+ dot_product = sum(x * y for x, y in zip(query_vector, node_vector))
+ norm_v1 = math.sqrt(sum(x ** 2 for x in query_vector))
+ norm_v2 = math.sqrt(sum(y ** 2 for y in node_vector))
+ return dot_product / (norm_v1 * norm_v2)
+
+ def retrieve_by_query(self, query: str, top_k: int = 3, **kwargs) -> List[VectorStoreNode]:
+ query_vector = self.embedding_model.get_embeddings(query)
+ nodes: List[VectorStoreNode] = self._load()
+ for node in nodes:
+ node.metadata["score"] = self.calculate_similarity(query_vector, node.vector)
+
+ nodes = sorted(nodes, key=lambda x: x.metadata["score"], reverse=True)
+ return nodes[:top_k]
diff --git a/experiencescope/tool/__init__.py b/experiencescope/tool/__init__.py
new file mode 100644
index 00000000..ebbcbd5d
--- /dev/null
+++ b/experiencescope/tool/__init__.py
@@ -0,0 +1,9 @@
+from beyondagent.core.tool.python_tools.code_tool import CodeTool
+from beyondagent.core.tool.python_tools.dashscope_search_tool import DashscopeSearchTool
+from beyondagent.core.tool.python_tools.terminate_tool import TerminateTool
+from beyondagent.core.utils.registry import Registry
+
+TOOL_REGISTRY = Registry("tools")
+TOOL_REGISTRY.register(CodeTool)
+TOOL_REGISTRY.register(DashscopeSearchTool)
+TOOL_REGISTRY.register(TerminateTool)
diff --git a/experiencescope/tool/base_tool.py b/experiencescope/tool/base_tool.py
new file mode 100644
index 00000000..4b3ffae5
--- /dev/null
+++ b/experiencescope/tool/base_tool.py
@@ -0,0 +1,79 @@
+from abc import ABC
+
+from loguru import logger
+from pydantic import BaseModel, Field
+
+
+class BaseTool(BaseModel, ABC):
+ tool_id: str = Field(default="")
+ name: str = Field(..., description="tool name")
+ description: str = Field(..., description="tool description")
+ tool_type: str = Field(default="function")
+ parameters: dict = Field(default_factory=dict, description="tool parameters")
+ arguments: dict = Field(default_factory=dict, description="execute arguments")
+
+ enable_cache: bool = Field(default=False, description="whether to cache the tool result")
+ # TODO add cache expire
+ cached_result: dict = Field(default_factory=dict, description="tool execution result")
+
+ max_retries: int = Field(default=3, description="max retries")
+ raise_exception: bool = Field(default=True, description="raise exception")
+ success: bool = Field(default=True, description="whether the tool executed successfully")
+
+ def reset(self):
+ self.arguments.clear()
+ self.success = True
+
+ def _execute(self, **kwargs):
+ raise NotImplementedError
+
+ def execute(self, **kwargs):
+ cache_id = ""
+ if self.enable_cache:
+ cache_id = self.get_cache_id(**kwargs)
+ if cache_id in self.cached_result:
+ return self.cached_result[cache_id]
+
+ for i in range(self.max_retries):
+ try:
+ if self.enable_cache:
+ self.cached_result[cache_id] = self._execute(**kwargs)
+ return self.cached_result[cache_id]
+
+ else:
+ return self._execute(**kwargs)
+
+ except Exception as e:
+ logger.exception(f"using tool.name={self.name} encounter error with e={e.args}")
+ if i == self.max_retries - 1 and self.raise_exception:
+ raise e
+
+ return None
+
+ # It may be in other different tool params formats; different versions are completed here.
+
+ @property
+ def simple_dict(self) -> dict:
+ return {
+ "type": self.tool_type,
+ self.tool_type: {
+ "name": self.name,
+ "description": self.description,
+ "parameters": self.parameters,
+ },
+ }
+
+ @property
+ def input_schema(self) -> dict:
+ return self.parameters.get("properties", {})
+
+ @property
+ def output_schema(self) -> dict:
+ raise NotImplementedError
+
+ def refresh(self):
+ # for mcp
+ raise NotImplementedError
+
+ def get_cache_id(self, **kwargs) -> str:
+ raise NotImplementedError
diff --git a/experiencescope/tool/code_tool.py b/experiencescope/tool/code_tool.py
new file mode 100644
index 00000000..6bdfd195
--- /dev/null
+++ b/experiencescope/tool/code_tool.py
@@ -0,0 +1,42 @@
+import sys
+from io import StringIO
+
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class CodeTool(BaseTool):
+ name: str = "python_execute"
+ description: str = "Execute python code can be used in scenarios such as analysis or calculation, and the final result can be printed using the `print` function."
+ parameters: dict = {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "description": "code to be executed. Please do not execute any matplotlib code here.",
+ }
+ },
+ "required": ["code"]
+ }
+
+ def _execute(self, code: str, **kwargs):
+ old_stdout = sys.stdout
+ redirected_output = sys.stdout = StringIO()
+
+ try:
+ exec(code)
+ result = redirected_output.getvalue()
+
+ except Exception as e:
+ self.success = False
+ result = str(e)
+
+ sys.stdout = old_stdout
+
+ return result
+
+
+
+if __name__ == '__main__':
+ tool = CodeTool()
+ print(tool.execute(code="print('Hello World')"))
+ print(tool.execute(code="print('Hello World!'"))
diff --git a/experiencescope/tool/dashscope_search_tool.py b/experiencescope/tool/dashscope_search_tool.py
new file mode 100644
index 00000000..f42b2b6f
--- /dev/null
+++ b/experiencescope/tool/dashscope_search_tool.py
@@ -0,0 +1,159 @@
+import os
+from typing import Literal
+
+import dashscope
+from dashscope.api_entities.dashscope_response import Message
+from loguru import logger
+from pydantic import Field
+
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class DashscopeSearchTool(BaseTool):
+ name: str = "web_search"
+ description: str = "Use search keywords to retrieve relevant information from the internet. " \
+ "If there are multiple search keywords, please use each keyword separately to call this tool."
+ parameters: dict = {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "search keyword",
+ }
+ },
+ "required": ["query"]
+ }
+
+ model_name: Literal["qwen-plus-2025-04-28", "qwq-plus-latest", "qwen-max-2025-01-25"] = \
+ Field(default="qwen-plus-2025-04-28")
+ api_key: str = Field(default_factory=lambda: os.environ["DASHSCOPE_API_KEY"])
+ stream_print: bool = Field(default=False)
+ temperature: float = Field(default=0.0000001)
+ use_role_prompt: bool = Field(default=True)
+ role_prompt: str = """
+# user's question
+{question}
+
+# task
+Extract the original content related to the user's question directly from the context, maintain accuracy, and avoid excessive processing. """.strip()
+ return_only_content: bool = Field(default=True)
+
+ def parse_reasoning_response(self, response, result: dict):
+ is_answering = False
+ is_first_chunk = True
+
+ for chunk in response:
+ if is_first_chunk:
+ result["search_results"] = chunk.output.search_info["search_results"]
+
+ if self.stream_print:
+ print("=" * 20 + "search result" + "=" * 20)
+ for web in result["search_results"]:
+ print(f"[{web['index']}]: [{web['title']}]({web['url']})")
+ print("=" * 20 + "thinking process" + "=" * 20)
+ result["reasoning_content"] += chunk.output.choices[0].message.reasoning_content
+
+ if self.stream_print:
+ print(chunk.output.choices[0].message.reasoning_content, end="", flush=True)
+ is_first_chunk = False
+
+ else:
+ if chunk.output.choices[0].message.content == "" \
+ and chunk.output.choices[0].message.reasoning_content == "":
+ pass
+
+ else:
+ if chunk.output.choices[0].message.reasoning_content != "" and \
+ chunk.output.choices[0].message.content == "":
+
+ if self.stream_print:
+ print(chunk.output.choices[0].message.reasoning_content, end="", flush=True)
+ result["reasoning_content"] += chunk.output.choices[0].message.reasoning_content
+
+ elif chunk.output.choices[0].message.content != "":
+ if not is_answering:
+ if self.stream_print:
+ print("\n" + "=" * 20 + "complete answer" + "=" * 20)
+ is_answering = True
+
+ if self.stream_print:
+ print(chunk.output.choices[0].message.content, end="", flush=True)
+ result["answer_content"] += chunk.output.choices[0].message.content
+
+ def parse_response(self, response, result: dict):
+ is_first_chunk = True
+
+ for chunk in response:
+ if is_first_chunk:
+ result["search_results"] = chunk.output.search_info["search_results"]
+
+ if self.stream_print:
+ print("=" * 20 + "search result" + "=" * 20)
+ for web in result["search_results"]:
+ print(f"[{web['index']}]: [{web['title']}]({web['url']})")
+ print("\n" + "=" * 20 + "complete answer" + "=" * 20)
+ is_first_chunk = False
+
+ else:
+ if chunk.output.choices[0].message.content == "":
+ pass
+
+ else:
+ if chunk.output.choices[0].message.content != "":
+ if self.stream_print:
+ print(chunk.output.choices[0].message.content, end="", flush=True)
+ result["answer_content"] += chunk.output.choices[0].message.content
+
+ def execute(self, query: str = "", **kwargs):
+ result = {
+ "search_results": [],
+ "reasoning_content": "",
+ "answer_content": ""
+ }
+ user_query = self.role_prompt.format(question=query) if self.use_role_prompt else query
+ messages = [Message(role="user", content=user_query)]
+
+ response = dashscope.Generation.call(
+ api_key=self.api_key,
+ model=self.model_name,
+ messages=messages,
+ enable_thinking=True,
+ enable_search=True,
+ search_options={
+ "forced_search": True,
+ "enable_source": True,
+ "enable_citation": False,
+ "search_strategy": "pro"
+ },
+ stream=True,
+ incremental_output=True,
+ result_format="message",
+ )
+
+ if self.model_name != "qwen-max-2025-01-25":
+ self.parse_reasoning_response(response, result)
+ else:
+ self.parse_response(response, result)
+
+ if self.return_only_content:
+ return result["answer_content"]
+ else:
+ return result
+
+def main():
+ from beyondagent.core.utils.test_key import set_key
+ set_key()
+ query = "What is artificial intelligence?"
+
+ tool = DashscopeSearchTool(stream_print=True)
+ logger.info(tool.execute(query=query))
+
+ tool = DashscopeSearchTool(stream_print=False)
+ logger.info(tool.execute(query=query))
+
+ tool = DashscopeSearchTool(stream_print=True, model_name="qwen-max-2025-01-25")
+ logger.info(tool.execute(query=query))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/experiencescope/tool/mcp_tool.py b/experiencescope/tool/mcp_tool.py
new file mode 100644
index 00000000..4e0e45a8
--- /dev/null
+++ b/experiencescope/tool/mcp_tool.py
@@ -0,0 +1,90 @@
+
+from mcp.client.sse import sse_client
+from mcp import ClientSession
+import asyncio
+from pydantic import Field
+from typing import List,Optional
+from loguru import logger
+
+
+from beyondagent.core.tool.base_tool import BaseTool
+
+class MCPTool(BaseTool):
+ server_url: str = Field(..., description="MCP server URL")
+ tool_name_list: List[str] = Field(default_factory=list)
+ cache_tools: dict = Field(default_factory=dict, alias="cache_tools")
+ cache_tools_info: Optional[dict] = Field(default=None, alias="cache_tools_info")
+
+ class Config:
+ underscore_attrs_are_private = True
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ self.refresh()
+
+ def get_tool_name_list(self) -> List[str]:
+ return self.tool_name_list
+
+ def get_server_info(self):
+ return self.cache_tools_info
+
+ def refresh(self):
+ self.cache_tools.clear()
+ self.tool_name_list.clear()
+
+ if "sse" in self.server_url:
+ original_tool_list = asyncio.run(self._get_tools())
+ self.cache_tools_info = original_tool_list.tools
+
+ for tool in self.cache_tools_info:
+ self.cache_tools[tool.name] = tool
+ self.tool_name_list.append(tool.name)
+ else:
+ # TODO: Implement non-SSE refresh logic
+ logger.warning("Non-SSE refresh not implemented yet")
+
+ async def _get_tools(self):
+ async with sse_client(url=self.server_url) as streams:
+ async with ClientSession(streams[0], streams[1]) as session:
+ await session.initialize()
+ tools = await session.list_tools()
+ return tools
+
+ def input_schema(self, tool_name: str) -> dict:
+ return self.cache_tools.get(tool_name, {}).inputSchema
+
+ def output_schema(self, tool_name: str) -> dict:
+ # TODO: Implement output schema logic
+ return {}
+
+ def get_tool_description(self, tool_name: str, schema: bool = False) -> str:
+ tool = self.cache_tools.get(tool_name)
+ if not tool:
+ return ""
+
+ description = f'tool \'{tool_name}\' description is:'+ tool.description
+ if schema:
+ description += f"\nInput Schema: {self.input_schema(tool_name)}"
+ description += f"\nOutput Schema: {self.output_schema(tool_name)}"
+ return description
+
+ async def _execute(self, **kwargs):
+ tool_name = kwargs.get('tool_name')
+ args = kwargs.get('args', {})
+
+ if "sse" in self.server_url:
+ async with sse_client(url=self.server_url) as streams:
+ async with ClientSession(streams[0], streams[1]) as session:
+ await session.initialize()
+ results = await session.call_tool(tool_name, args)
+ return results.content[0].text, results.isError
+ else:
+ return "Failed to connect to the tool", False
+
+ def execute(self, **kwargs):
+ return asyncio.run(self._execute(**kwargs))
+
+ def get_cache_id(self, **kwargs) -> str:
+ # Implement a method to generate a unique cache ID based on the input
+ return f"{kwargs.get('tool_name')}_{hash(frozenset(kwargs.get('args', {}).items()))}"
+
diff --git a/experiencescope/tool/terminate_tool.py b/experiencescope/tool/terminate_tool.py
new file mode 100644
index 00000000..c29c8931
--- /dev/null
+++ b/experiencescope/tool/terminate_tool.py
@@ -0,0 +1,23 @@
+from beyondagent.core.tool.base_tool import BaseTool
+
+
+class TerminateTool(BaseTool):
+ name: str = "terminate"
+ description: str = "If you can answer the user's question based on the context, be sure to use the **terminate** tool."
+ parameters: dict = {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "description": "Please determine whether the user's question has been completed. (success / failure)",
+ "enum": ["success", "failure"],
+ }
+ },
+ "required": ["status"],
+ }
+
+ def execute(self, status: str):
+ self.success = status in ["success", "failure"]
+ return f"The interaction has been completed with status: {status}"
+
+
diff --git a/experiencescope/utils/__init__.py b/experiencescope/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/experiencescope/utils/file_handler.py b/experiencescope/utils/file_handler.py
new file mode 100644
index 00000000..66850ac9
--- /dev/null
+++ b/experiencescope/utils/file_handler.py
@@ -0,0 +1,29 @@
+import json
+from pathlib import Path
+from typing import Any
+
+import yaml
+from pydantic import BaseModel, Field, PrivateAttr
+
+
+class FileHandler(BaseModel):
+ file_path: str = Field(default=...)
+ _obj: Any = PrivateAttr()
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ suffix = Path(self.file_path).suffix
+ if suffix == ".json":
+ self._obj = json
+ elif suffix == ".yaml":
+ self._obj = yaml
+ else:
+ raise ValueError(f"unsupported file type={suffix}")
+
+ def dump(self, config, **kwargs):
+ with open(self.file_path, "w") as f:
+ self._obj.dump(config, f, **kwargs)
+
+ def load(self, **kwargs):
+ with open(self.file_path, "r") as f:
+ return self._obj.load(f, **kwargs)
diff --git a/experiencescope/utils/http_client.py b/experiencescope/utils/http_client.py
new file mode 100644
index 00000000..9b70ee00
--- /dev/null
+++ b/experiencescope/utils/http_client.py
@@ -0,0 +1,152 @@
+import http
+import time
+from typing import Any
+
+import requests
+from loguru import logger
+from pydantic import BaseModel, Field, PrivateAttr
+
+from beyondagent.core.enumeration.http_enum import HttpEnum
+
+
+class HttpClient(BaseModel):
+ url: str = Field(default="")
+ keep_alive: bool = Field(default=False, description="if true, use session to keep long connection")
+ timeout: int = Field(default=300, description="request timeout, second")
+
+ return_default_if_error: bool = Field(default=True)
+ request_start_time: float = Field(default_factory=time.time)
+ request_time_cost: float = Field(default=0.0, description="request time cost")
+
+ retry_sleep_time: float = Field(0.5, description="interval time for retry")
+ retry_time_multiplier: float = Field(2.0, description="retry time multiplier")
+ retry_max_count: int = Field(1, description="maximum number of retries")
+
+ _client: Any = PrivateAttr()
+
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self._client = requests.Session() if self.keep_alive else requests
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+ self.request_time_cost: float = time.time() - self.request_start_time
+
+ def close(self):
+ if isinstance(self._client, requests.Session):
+ self._client.close()
+
+ def _request(self,
+ data: str = None,
+ json_data: dict = None,
+ headers: dict = None,
+ stream: bool = False,
+ http_enum: HttpEnum | str = HttpEnum.POST):
+
+ if isinstance(http_enum, str):
+ http_enum = HttpEnum(http_enum)
+
+ if http_enum is HttpEnum.POST:
+ response: requests.Response = self._client.post(url=self.url,
+ data=data,
+ json=json_data,
+ headers=headers,
+ stream=stream,
+ timeout=self.timeout)
+
+ elif http_enum is HttpEnum.GET:
+ response: requests.Response = self._client.get(url=self.url,
+ data=data,
+ json=json_data,
+ headers=headers,
+ stream=stream,
+ timeout=self.timeout)
+
+ else:
+ raise NotImplementedError
+
+ if response.status_code != http.HTTPStatus.OK:
+ raise RuntimeError(f"request failed! content={response.json()}")
+
+ return response
+
+ def parse_result(self, response: requests.Response | Any = None, **kwargs):
+ return response.json()
+
+ def return_default(self, **kwargs):
+ return None
+
+ def request(self,
+ data: str | Any = None,
+ json_data: dict = None,
+ headers: dict = None,
+ http_enum: HttpEnum | str = HttpEnum.POST,
+ **kwargs):
+
+ retry_sleep_time = self.retry_sleep_time
+ for i in range(self.retry_max_count):
+ try:
+ response = self._request(data=data, json_data=json_data, headers=headers, http_enum=http_enum)
+ result = self.parse_result(response=response,
+ data=data,
+ json_data=json_data,
+ headers=headers,
+ http_enum=http_enum,
+ **kwargs)
+ return result
+
+ except Exception as e:
+ logger.exception(f"{self.__class__.__name__} {i}th request failed with args={e.args}")
+
+ if i == self.retry_max_count - 1:
+ if self.return_default_if_error:
+ return self.return_default()
+ else:
+ raise e
+
+ retry_sleep_time *= self.retry_time_multiplier
+ time.sleep(retry_sleep_time)
+
+ def request_stream(self,
+ data: str = None,
+ json_data: dict = None,
+ headers: dict = None,
+ http_enum: HttpEnum | str = HttpEnum.POST,
+ **kwargs):
+
+ retry_sleep_time = self.retry_sleep_time
+ for i in range(self.retry_max_count):
+ try:
+ response = self._request(data=data,
+ json_data=json_data,
+ headers=headers,
+ stream=True,
+ http_enum=http_enum)
+ request_context = {}
+ for iter_idx, line in enumerate(response.iter_lines()):
+ yield self.parse_result(line=line,
+ request_context=request_context,
+ index=iter_idx,
+ data=data,
+ json_data=json_data,
+ headers=headers,
+ http_enum=http_enum,
+ **kwargs)
+
+ return
+
+ except Exception as e:
+ logger.exception(f"{self.__class__.__name__} {i}th request failed with args={e.args}")
+
+ if i == self.retry_max_count - 1:
+ if self.return_default_if_error:
+ return self.return_default()
+ else:
+ raise e
+
+ retry_sleep_time *= self.retry_time_multiplier
+ time.sleep(retry_sleep_time)
diff --git a/experiencescope/utils/logger.py b/experiencescope/utils/logger.py
new file mode 100644
index 00000000..36c4f69f
--- /dev/null
+++ b/experiencescope/utils/logger.py
@@ -0,0 +1,11 @@
+from best_logger import register_logger
+
+def init_logger():
+ register_logger(
+ mods=["agent", "context", "summary"],
+ non_console_mods=[],
+ auto_clean_mods=[],
+ base_log_path=f"logs/default"
+ )
+
+
diff --git a/experiencescope/utils/prompt_handler.py b/experiencescope/utils/prompt_handler.py
new file mode 100644
index 00000000..fe68956e
--- /dev/null
+++ b/experiencescope/utils/prompt_handler.py
@@ -0,0 +1,70 @@
+import os
+
+import yaml
+from loguru import logger
+from pydantic import BaseModel, Field
+
+
+class PromptHandler(BaseModel):
+ dir_path: str = Field(default="")
+ prompt_dict: dict = Field(default_factory=dict)
+
+ def add_prompt_file(self, file_name: str):
+ prompt_path = os.path.join(self.dir_path, file_name + ".yaml")
+ self._add_prompt_file(prompt_path)
+
+ def _add_prompt_file(self, prompt_path: str):
+ if os.path.exists(prompt_path):
+ with open(prompt_path) as f:
+ prompt_dict: dict = yaml.load(f, yaml.FullLoader)
+ self.update_prompt_dict(prompt_dict)
+ else:
+ logger.warning(f"prompt_path={prompt_path} not exists!")
+
+ def update_prompt_dict(self, prompt_dict: dict):
+ self.prompt_dict.update(prompt_dict)
+
+ def __getitem__(self, key: str):
+ return self.prompt_dict[key]
+
+ def __setitem__(self, key: str, value: str):
+ self.prompt_dict[key] = value
+
+ def __getattr__(self, key: str):
+ if key in self.prompt_dict:
+ return self.prompt_dict[key]
+
+ return super().__getattr__(key)
+
+ def prompt_format(self, prompt_name: str, **kwargs):
+ prompt = self.prompt_dict[prompt_name]
+
+ flag_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, bool)}
+ other_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)}
+
+ if flag_kwargs:
+ split_prompt = []
+ for line in prompt.strip().split("\n"):
+ hit = False
+ hit_flag = True
+ for key, flag in kwargs.items():
+ if not line.startswith(f"[{key}]"):
+ continue
+
+ else:
+ hit = True
+ hit_flag = flag
+ line = line.strip(f"[{key}]")
+ break
+
+ if not hit:
+ split_prompt.append(line)
+ elif hit_flag:
+ split_prompt.append(line)
+
+ prompt = "\n".join(split_prompt)
+
+ if other_kwargs:
+ prompt = prompt.format(**other_kwargs)
+
+ return prompt
diff --git a/experiencescope/utils/registry.py b/experiencescope/utils/registry.py
new file mode 100644
index 00000000..65199cda
--- /dev/null
+++ b/experiencescope/utils/registry.py
@@ -0,0 +1,32 @@
+from typing import Dict, Any, List
+
+
+class Registry(object):
+
+ def __init__(self, name: str):
+ self.name: str = name
+ self.module_dict: Dict[str, Any] = {}
+
+ def register(self, module, module_name: str = 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]):
+ 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):
+ assert module_name in self.module_dict, f"{module_name} not found in {self.name}"
+ return self.module_dict[module_name]
diff --git a/experiencescope/utils/test_key.py b/experiencescope/utils/test_key.py
new file mode 100644
index 00000000..de2c5d07
--- /dev/null
+++ b/experiencescope/utils/test_key.py
@@ -0,0 +1,33 @@
+import json
+import os
+
+from beyondagent.core.schema.module_loader import ModuleLoader
+
+
+def load_env_keys():
+ if os.path.exists(".env"):
+ with open(".env") as f:
+ config = json.load(f)
+ for k, v in config.items():
+ os.environ[k] = v
+
+
+agent_wrapper_loader = ModuleLoader(
+ class_path="beyondagent.core.module.agent_wrapper.naive_agent_wrapper",
+ class_name="NaiveAgentWrapper",
+ config_path="beyondagent/config/agent_wrapper/naive_agent_wrapper.json")
+
+context_generator_loader = ModuleLoader(
+ class_path="beyondagent.core.module.context_generator.simple_context_generator",
+ class_name="SimpleContextGenerator",
+ config_path="beyondagent/config/context_generator/simple_context_generator.json")
+
+summarizer_loader = ModuleLoader(
+ class_path="beyondagent.core.module.summarizer.simple_summarizer",
+ class_name="SimpleSummarizer",
+ config_path="beyondagent/config/summarizer/simple_summarizer.json")
+
+env_loader = ModuleLoader(
+ class_path="beyondagent.core.module.environment.simple_environment",
+ class_name="SimpleEnvironment",
+ config_path="beyondagent/config/environment/simple_environment.json")
\ No newline at end of file
diff --git a/experiencescope/utils/trajectory_utils.py b/experiencescope/utils/trajectory_utils.py
new file mode 100644
index 00000000..33cecf5f
--- /dev/null
+++ b/experiencescope/utils/trajectory_utils.py
@@ -0,0 +1,18 @@
+from typing import List
+
+
+from beyondagent.core.schema.trajectory import Message, StateMessage, ActionMessage
+
+def format_trajectory_steps(steps: List[Message]) -> str:
+ format_steps = []
+ step_idx = 0
+ single_step = []
+ for idx, step in enumerate(steps):
+ if isinstance(step, ActionMessage):
+ step_idx += 1
+ single_step.append(f"** STEP {step_idx} **\n{step.content}")
+ elif isinstance(step, StateMessage):
+ single_step.append(f"{step.content}")
+ format_steps.append("\n".join(single_step))
+ single_step = []
+ return "\n\n".join(format_steps)
\ No newline at end of file
diff --git a/experiencescope/utils/util_function.py b/experiencescope/utils/util_function.py
new file mode 100644
index 00000000..3427cf26
--- /dev/null
+++ b/experiencescope/utils/util_function.py
@@ -0,0 +1,9 @@
+import re
+
+
+def get_html_match_content(content: str, key: str):
+ pattern = rf"<{key}>(.*?){key}>"
+ match = re.search(pattern, content, re.DOTALL)
+ if match:
+ return match.group(1)
+ return None
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..7f2199a7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,30 @@
+[project]
+name = "beyond-agent"
+version = "0.1.1"
+description = "Agent "
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "beast-logger",
+ "anthropic>=0.45.1",
+ "argparse>=1.4.0",
+ "httpx>=0.28.1",
+ "mcp[cli]>=1.2.1",
+ "python-dotenv>=1.0.1",
+ "firecrawl-py>=1.14.1",
+ "loguru",
+ "pillow",
+ "regex",
+ "fastapi",
+ "arxiv",
+ "tqdm",
+ "requests_oauthlib",
+ "teamwork-mcp>=0.2.1",
+ "agentscope",
+ "beyond @ file:///${PROJECT_ROOT}/beyondagent/third_party/beyond",
+ "astor",
+]
+
+
+[tool.setuptools.packages.find]
+include = ["beyondagent"]
\ No newline at end of file