This commit is contained in:
jinli.yl 2025-06-05 19:21:01 +08:00
parent 8f3dce2932
commit ef224979cc
65 changed files with 2413 additions and 0 deletions

21
.gitignore vendored Normal file
View 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

View file

@ -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
View file

View file

View file

View file

@ -0,0 +1,8 @@
from enum import Enum
class AgentState(str, Enum):
IDLE = "idle"
RUNNING = "running"
COMPLETE = "complete"
FAILED = "failed"

View file

@ -0,0 +1,9 @@
from enum import Enum
class ChunkEnum(str, Enum):
THINK = "think"
ANSWER = "answer"
TOOL = "tool"
USAGE = "usage"
ERROR = "error"

View file

@ -0,0 +1,6 @@
from enum import Enum
class HttpEnum(str, Enum):
GET = "get"
POST = "post"

View 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

View 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")

View 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)}")

View 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

View 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)}")

View 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="")

View file

View 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)

View file

@ -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

View 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)

View 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

View 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")

View 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}

View 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

View 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

View 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

View 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 []

View 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
"""

View file

View 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()

View 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)

View 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)

View 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)

View 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()

View 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)

View file

View 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

View 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"}))

View file

View 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

View 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

View 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

View 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]

View 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)

View 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

View 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!'"))

View 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()

View 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()))}"

View 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}"

View file

View 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)

View 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)

View 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"
)

View 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

View 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]

View 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")

View 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)

View 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
View 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"]