mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-07 03:00:27 +00:00
init
This commit is contained in:
parent
8f3dce2932
commit
ef224979cc
65 changed files with 2413 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
42
README.md
42
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
|
||||
0
cookbook/__init__.py
Normal file
0
cookbook/__init__.py
Normal file
0
experiencescope/__init__.py
Normal file
0
experiencescope/__init__.py
Normal file
0
experiencescope/enumeration/__init__.py
Normal file
0
experiencescope/enumeration/__init__.py
Normal file
8
experiencescope/enumeration/agent_state.py
Normal file
8
experiencescope/enumeration/agent_state.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class AgentState(str, Enum):
|
||||
IDLE = "idle"
|
||||
RUNNING = "running"
|
||||
COMPLETE = "complete"
|
||||
FAILED = "failed"
|
||||
9
experiencescope/enumeration/chunk_enum.py
Normal file
9
experiencescope/enumeration/chunk_enum.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class ChunkEnum(str, Enum):
|
||||
THINK = "think"
|
||||
ANSWER = "answer"
|
||||
TOOL = "tool"
|
||||
USAGE = "usage"
|
||||
ERROR = "error"
|
||||
6
experiencescope/enumeration/http_enum.py
Normal file
6
experiencescope/enumeration/http_enum.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class HttpEnum(str, Enum):
|
||||
GET = "get"
|
||||
POST = "post"
|
||||
11
experiencescope/enumeration/role.py
Normal file
11
experiencescope/enumeration/role.py
Normal file
|
|
@ -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
|
||||
10
experiencescope/model/__init__.py
Normal file
10
experiencescope/model/__init__.py
Normal file
|
|
@ -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")
|
||||
86
experiencescope/model/base_embedding_model.py
Normal file
86
experiencescope/model/base_embedding_model.py
Normal file
|
|
@ -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)}")
|
||||
110
experiencescope/model/base_llm.py
Normal file
110
experiencescope/model/base_llm.py
Normal file
|
|
@ -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
|
||||
73
experiencescope/model/openai_compatible_embedding_model.py
Normal file
73
experiencescope/model/openai_compatible_embedding_model.py
Normal file
|
|
@ -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)}")
|
||||
156
experiencescope/model/openai_compatible_llm.py
Normal file
156
experiencescope/model/openai_compatible_llm.py
Normal file
|
|
@ -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<usage>{chunk.model_dump_json(indent=2)}</usage>")
|
||||
else:
|
||||
print(f"\n<usage>{chunk}</usage>")
|
||||
|
||||
elif chunk_enum is ChunkEnum.THINK:
|
||||
if not enter_think:
|
||||
enter_think = True
|
||||
print("<think>\n", end="")
|
||||
print(chunk, end="")
|
||||
|
||||
elif chunk_enum is ChunkEnum.ANSWER:
|
||||
if not enter_answer:
|
||||
enter_answer = True
|
||||
if enter_think:
|
||||
print("\n</think>")
|
||||
print(chunk, end="")
|
||||
|
||||
elif chunk_enum is ChunkEnum.TOOL:
|
||||
assert isinstance(chunk, ToolCall)
|
||||
print(f"\n<tool>{chunk.model_dump_json(indent=2)}</tool>", end="")
|
||||
|
||||
elif chunk_enum is ChunkEnum.ERROR:
|
||||
print(f"\n<error>{chunk}</error>", end="")
|
||||
0
experiencescope/module/__init__.py
Normal file
0
experiencescope/module/__init__.py
Normal file
0
experiencescope/module/agent_wrapper/__init__.py
Normal file
0
experiencescope/module/agent_wrapper/__init__.py
Normal file
91
experiencescope/module/agent_wrapper/base_agent_wrapper.py
Normal file
91
experiencescope/module/agent_wrapper/base_agent_wrapper.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
62
experiencescope/module/agent_wrapper/naive_agent_wrapper.py
Normal file
62
experiencescope/module/agent_wrapper/naive_agent_wrapper.py
Normal file
|
|
@ -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)
|
||||
48
experiencescope/module/base_module.py
Normal file
48
experiencescope/module/base_module.py
Normal file
|
|
@ -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
|
||||
0
experiencescope/module/context_generator/__init__.py
Normal file
0
experiencescope/module/context_generator/__init__.py
Normal file
|
|
@ -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")
|
||||
0
experiencescope/module/environment/__init__.py
Normal file
0
experiencescope/module/environment/__init__.py
Normal file
68
experiencescope/module/environment/base_environment.py
Normal file
68
experiencescope/module/environment/base_environment.py
Normal file
|
|
@ -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}
|
||||
0
experiencescope/module/evaluator/__init__.py
Normal file
0
experiencescope/module/evaluator/__init__.py
Normal file
19
experiencescope/module/evaluator/base_evaluator.py
Normal file
19
experiencescope/module/evaluator/base_evaluator.py
Normal file
|
|
@ -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
|
||||
0
experiencescope/module/reward_fn/__init__.py
Normal file
0
experiencescope/module/reward_fn/__init__.py
Normal file
11
experiencescope/module/reward_fn/base_reward_fn.py
Normal file
11
experiencescope/module/reward_fn/base_reward_fn.py
Normal file
|
|
@ -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
|
||||
0
experiencescope/module/runner/__init__.py
Normal file
0
experiencescope/module/runner/__init__.py
Normal file
29
experiencescope/module/runner/base_runner.py
Normal file
29
experiencescope/module/runner/base_runner.py
Normal file
|
|
@ -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
|
||||
0
experiencescope/module/summarizer/__init__.py
Normal file
0
experiencescope/module/summarizer/__init__.py
Normal file
37
experiencescope/module/summarizer/base_summarizer.py
Normal file
37
experiencescope/module/summarizer/base_summarizer.py
Normal file
|
|
@ -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 []
|
||||
0
experiencescope/module/trainner/__init__.py
Normal file
0
experiencescope/module/trainner/__init__.py
Normal file
45
experiencescope/module/trainner/base_trainner.py
Normal file
45
experiencescope/module/trainner/base_trainner.py
Normal file
|
|
@ -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
|
||||
"""
|
||||
0
experiencescope/schema/__init__.py
Normal file
0
experiencescope/schema/__init__.py
Normal file
19
experiencescope/schema/module_loader.py
Normal file
19
experiencescope/schema/module_loader.py
Normal file
|
|
@ -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()
|
||||
24
experiencescope/schema/request.py
Normal file
24
experiencescope/schema/request.py
Normal file
|
|
@ -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)
|
||||
23
experiencescope/schema/response.py
Normal file
23
experiencescope/schema/response.py
Normal file
|
|
@ -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)
|
||||
6
experiencescope/schema/reward.py
Normal file
6
experiencescope/schema/reward.py
Normal file
|
|
@ -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)
|
||||
127
experiencescope/schema/trajectory.py
Normal file
127
experiencescope/schema/trajectory.py
Normal file
|
|
@ -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()
|
||||
12
experiencescope/schema/vector_store_node.py
Normal file
12
experiencescope/schema/vector_store_node.py
Normal file
|
|
@ -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)
|
||||
0
experiencescope/service/__init__.py
Normal file
0
experiencescope/service/__init__.py
Normal file
42
experiencescope/service/model_service.py
Normal file
42
experiencescope/service/model_service.py
Normal file
|
|
@ -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
|
||||
24
experiencescope/service/model_service_client.py
Normal file
24
experiencescope/service/model_service_client.py
Normal file
|
|
@ -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"}))
|
||||
0
experiencescope/storage/__init__.py
Normal file
0
experiencescope/storage/__init__.py
Normal file
17
experiencescope/storage/base_sample_buffer.py
Normal file
17
experiencescope/storage/base_sample_buffer.py
Normal file
|
|
@ -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
|
||||
26
experiencescope/storage/base_vector_store.py
Normal file
26
experiencescope/storage/base_vector_store.py
Normal file
|
|
@ -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
|
||||
169
experiencescope/storage/es_vector_store.py
Normal file
169
experiencescope/storage/es_vector_store.py
Normal file
|
|
@ -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
|
||||
132
experiencescope/storage/file_vector_store.py
Normal file
132
experiencescope/storage/file_vector_store.py
Normal file
|
|
@ -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]
|
||||
9
experiencescope/tool/__init__.py
Normal file
9
experiencescope/tool/__init__.py
Normal file
|
|
@ -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)
|
||||
79
experiencescope/tool/base_tool.py
Normal file
79
experiencescope/tool/base_tool.py
Normal file
|
|
@ -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
|
||||
42
experiencescope/tool/code_tool.py
Normal file
42
experiencescope/tool/code_tool.py
Normal file
|
|
@ -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!'"))
|
||||
159
experiencescope/tool/dashscope_search_tool.py
Normal file
159
experiencescope/tool/dashscope_search_tool.py
Normal file
|
|
@ -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()
|
||||
90
experiencescope/tool/mcp_tool.py
Normal file
90
experiencescope/tool/mcp_tool.py
Normal file
|
|
@ -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()))}"
|
||||
|
||||
23
experiencescope/tool/terminate_tool.py
Normal file
23
experiencescope/tool/terminate_tool.py
Normal file
|
|
@ -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}"
|
||||
|
||||
|
||||
0
experiencescope/utils/__init__.py
Normal file
0
experiencescope/utils/__init__.py
Normal file
29
experiencescope/utils/file_handler.py
Normal file
29
experiencescope/utils/file_handler.py
Normal file
|
|
@ -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)
|
||||
152
experiencescope/utils/http_client.py
Normal file
152
experiencescope/utils/http_client.py
Normal file
|
|
@ -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)
|
||||
11
experiencescope/utils/logger.py
Normal file
11
experiencescope/utils/logger.py
Normal file
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
70
experiencescope/utils/prompt_handler.py
Normal file
70
experiencescope/utils/prompt_handler.py
Normal file
|
|
@ -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
|
||||
32
experiencescope/utils/registry.py
Normal file
32
experiencescope/utils/registry.py
Normal file
|
|
@ -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]
|
||||
33
experiencescope/utils/test_key.py
Normal file
33
experiencescope/utils/test_key.py
Normal file
|
|
@ -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")
|
||||
18
experiencescope/utils/trajectory_utils.py
Normal file
18
experiencescope/utils/trajectory_utils.py
Normal file
|
|
@ -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)
|
||||
9
experiencescope/utils/util_function.py
Normal file
9
experiencescope/utils/util_function.py
Normal file
|
|
@ -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
|
||||
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
|
|
@ -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"]
|
||||
Loading…
Add table
Reference in a new issue