move code

This commit is contained in:
jinli.yl 2025-07-10 15:34:55 +08:00
parent d82af71fed
commit f377d3a8a9
29 changed files with 139 additions and 866 deletions

View file

@ -1,24 +0,0 @@
from pydantic import Field
from experiencemaker.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest
from experiencemaker.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse
from experiencemaker.utils.http_client import HttpClient
class EMClient(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

@ -1,297 +0,0 @@
import argparse
import copy
import json
import types
from typing import List
import uvicorn
from fastapi import FastAPI
from loguru import logger
from pydantic import BaseModel, Field, model_validator
from experiencemaker.utils.util_function import load_env_keys
load_env_keys()
from experiencemaker.model.base_embedding_model import BaseEmbeddingModel, EMBEDDING_MODEL_REGISTRY
from experiencemaker.model.base_llm import BaseLLM, LLM_REGISTRY
from experiencemaker.module.agent_wrapper.agent_wrapper_mixin import AgentWrapperMixin, AGENT_WRAPPER_REGISTRY
from experiencemaker.module.context_generator.base_context_generator import BaseContextGenerator, \
CONTEXT_GENERATOR_REGISTRY
from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer, SUMMARIZER_REGISTRY
from experiencemaker.schema.experience import Experience
from experiencemaker.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest
from experiencemaker.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse
from experiencemaker.schema.trajectory import Trajectory, ContextMessage
from experiencemaker.storage.base_vector_store import BaseVectorStore, VECTOR_STORE_REGISTRY
class EMService(BaseModel):
host: str = Field(default="0.0.0.0")
port: int = Field(default=8001)
timeout_keep_alive: int = Field(default=600000)
limit_concurrency: int = Field(default=128)
llm: BaseLLM | None = Field(default=None)
embedding_model: BaseEmbeddingModel | None = Field(default=None)
vector_store: BaseVectorStore | None = Field(default=None)
agent_wrapper: AgentWrapperMixin | None = Field(default=None)
context_generator: BaseContextGenerator | None = Field(default=None)
summarizer: BaseSummarizer | None = Field(default=None)
origin_config: dict = Field(default_factory=dict)
@staticmethod
def init_llm(llm_config: dict) -> BaseLLM:
backend = llm_config.pop("backend", None)
assert backend is not None, "llm must have a backend like `openai_compatible`."
assert backend in LLM_REGISTRY, f"llm backend={backend} not supported. " \
f"supported={LLM_REGISTRY.registered_module_names}"
llm = LLM_REGISTRY[backend](**llm_config)
logger.info(f"llm is inited with backend={backend} params={llm_config}")
return llm
@classmethod
def get_llm(cls, config: dict, llm: BaseLLM = None) -> BaseLLM:
if "llm" in config:
llm_config = config.pop("llm")
llm = cls.init_llm(llm_config)
elif llm is None:
raise RuntimeError("llm must be provided.")
return llm
@staticmethod
def init_embedding_model(embedding_model_config: dict) -> BaseEmbeddingModel:
backend = embedding_model_config.pop("backend", None)
assert backend is not None, "embedding_model must have a backend like `openai_compatible`."
assert backend in EMBEDDING_MODEL_REGISTRY, f"embedding_model backend={backend} not supported. " \
f"supported={EMBEDDING_MODEL_REGISTRY.registered_module_names}"
embedding_model = EMBEDDING_MODEL_REGISTRY[backend](**embedding_model_config)
logger.info(f"embedding_model is inited with backend={backend} params={embedding_model_config}")
return embedding_model
@classmethod
def get_embedding_model(cls, config: dict, embedding_model: BaseEmbeddingModel = None) -> BaseEmbeddingModel:
if "embedding_model" in config:
embedding_model_config = config.pop("embedding_model")
embedding_model = cls.init_embedding_model(embedding_model_config)
elif embedding_model is None:
raise RuntimeError("embedding_model must be provided.")
return embedding_model
@classmethod
def init_vector_store(cls, vector_store_config: dict,
embedding_model: BaseEmbeddingModel = None) -> BaseVectorStore:
backend = vector_store_config.pop("backend", None)
assert backend is not None, "vector_store must have a backend like `elasticsearch`."
assert backend in VECTOR_STORE_REGISTRY, f"vector_store backend={backend} not supported. " \
f"supported={VECTOR_STORE_REGISTRY.registered_module_names}"
embedding_model = cls.get_embedding_model(vector_store_config, embedding_model=embedding_model)
vector_store = VECTOR_STORE_REGISTRY[backend](**vector_store_config, embedding_model=embedding_model)
logger.info(f"vector_store is inited with backend={backend} params={vector_store_config}")
return vector_store
@classmethod
def get_vector_store(cls, config: dict, vector_store: BaseVectorStore = None,
embedding_model: BaseEmbeddingModel = None) -> BaseVectorStore:
if "vector_store" in config:
vector_store_config = config.pop("vector_store")
vector_store = cls.init_vector_store(vector_store_config, embedding_model=embedding_model)
elif vector_store is None:
raise RuntimeError("vector_store must be provided.")
return vector_store
@classmethod
def init_context_generator(cls, context_generator_config: dict, data: dict) -> BaseContextGenerator:
backend = context_generator_config.pop("backend", None)
assert backend is not None, "context_generator must have a backend like `simple`."
assert backend in CONTEXT_GENERATOR_REGISTRY, f"context_generator backend={backend} not supported. " \
f"supported={CONTEXT_GENERATOR_REGISTRY.registered_module_names}"
llm = cls.get_llm(context_generator_config, llm=data.get("llm"))
vector_store = cls.get_vector_store(context_generator_config, vector_store=data.get("vector_store"),
embedding_model=data.get("embedding_model"))
context_generator: BaseContextGenerator = CONTEXT_GENERATOR_REGISTRY[backend](
**context_generator_config, llm=llm, vector_store=vector_store)
logger.info(f"context_generator is inited with backend={backend} params={context_generator_config}")
return context_generator
@classmethod
def init_summarizer(cls, summarizer_config: dict, data: dict) -> BaseSummarizer:
backend = summarizer_config.pop("backend", None)
assert backend is not None, "summarizer must have a backend like `simple`."
assert backend in SUMMARIZER_REGISTRY, f"summarizer backend={backend} not supported. " \
f"supported={SUMMARIZER_REGISTRY.registered_module_names}"
llm = cls.get_llm(summarizer_config, llm=data.get("llm"))
vector_store = cls.get_vector_store(summarizer_config, vector_store=data.get("vector_store"),
embedding_model=data.get("embedding_model"))
summarizer: BaseSummarizer = SUMMARIZER_REGISTRY[backend](
**summarizer_config, llm=llm, vector_store=vector_store)
logger.info(f"summarizer is inited with backend={backend} params={summarizer_config}")
return summarizer
@classmethod
def init_agent_wrapper(cls, agent_wrapper_config: dict, data: dict) -> AgentWrapperMixin:
backend = agent_wrapper_config.pop("backend", None)
assert backend is not None, "agent_wrapper must have a backend like `simple`."
assert backend in AGENT_WRAPPER_REGISTRY, f"agent_wrapper backend={backend} not supported. " \
f"supported={AGENT_WRAPPER_REGISTRY.registered_module_names}"
llm = cls.get_llm(agent_wrapper_config, llm=data.get("llm"))
agent_wrapper: AgentWrapperMixin = AGENT_WRAPPER_REGISTRY[backend](
**agent_wrapper_config, llm=llm, context_generator=data.get("context_generator"))
logger.info(f"agent_wrapper is inited with backend={backend} params={agent_wrapper_config}")
return agent_wrapper
@classmethod
def init_class_by_config(cls, data: dict):
try:
if "llm" in data:
data["llm"] = cls.init_llm(data["llm"])
if "embedding_model" in data:
data["embedding_model"] = cls.init_embedding_model(data["embedding_model"])
if "vector_store" in data:
data["vector_store"] = cls.init_vector_store(data["vector_store"],
embedding_model=data["embedding_model"])
if "context_generator" in data:
data["context_generator"] = cls.init_context_generator(data["context_generator"], data)
if "summarizer" in data:
data["summarizer"] = cls.init_summarizer(data["summarizer"], data)
if "agent_wrapper" in data:
data["agent_wrapper"] = cls.init_agent_wrapper(data["agent_wrapper"], data)
except Exception as e:
logger.exception(e.args)
return data
@model_validator(mode="before") # noqa
@classmethod
def init_modules(cls, data: dict):
origin_config = copy.deepcopy(data)
data = cls.init_class_by_config(data)
data["origin_config"] = origin_config
return data
def call_agent_wrapper(self, request: AgentWrapperRequest) -> AgentWrapperResponse:
if "em_config" in request.metadata:
new_config = copy.deepcopy(self.origin_config)
new_config.update(request.metadata["em_config"])
data = EMService.init_class_by_config(new_config)
agent_wrapper = data["agent_wrapper"]
else:
assert self.agent_wrapper is not None, "agent_wrapper must be provided."
agent_wrapper = self.agent_wrapper
trajectory: Trajectory = agent_wrapper.execute(query=request.query,
workspace_id=request.workspace_id,
**request.metadata)
return AgentWrapperResponse(trajectory=trajectory)
def call_context_generator(self, request: ContextGeneratorRequest) -> ContextGeneratorResponse:
if "em_config" in request.metadata:
new_config = copy.deepcopy(self.origin_config)
new_config.update(request.metadata["em_config"])
data = EMService.init_class_by_config(new_config)
context_generator = data["context_generator"]
else:
assert self.context_generator is not None, "context_generator must be provided."
context_generator = self.context_generator
logger.info(f"workspace_id={request.workspace_id} metadata={request.metadata} "
f"trajectory=\n{request.trajectory.model_dump_json(indent=2)}")
context_msg: ContextMessage = context_generator.execute(trajectory=request.trajectory,
workspace_id=request.workspace_id,
**request.metadata)
logger.info(f"workspace_id={request.workspace_id} context_msg={context_msg.model_dump_json(indent=2)}")
return ContextGeneratorResponse(context_msg=context_msg)
def call_summarizer(self, request: SummarizerRequest) -> SummarizerResponse:
if "em_config" in request.metadata:
new_config = copy.deepcopy(self.origin_config)
new_config.update(request.metadata["em_config"])
data = EMService.init_class_by_config(new_config)
summarizer = data["summarizer"]
else:
assert self.summarizer is not None, "summarizer must be provided."
summarizer = self.summarizer
trajectories_content = "\n".join([x.model_dump_json(indent=2) for x in request.trajectories])
logger.info(f"workspace_id={request.workspace_id} metadata={request.metadata} "
f"trajectories=\n{trajectories_content}")
experiences: List[Experience] = summarizer.execute(trajectories=request.trajectories,
workspace_id=request.workspace_id,
**request.metadata)
experiences_content = "\n".join([x.model_dump_json(indent=2) for x in experiences])
logger.info(f"workspace_id={request.workspace_id} experiences_content=\n{experiences_content}")
return SummarizerResponse(experiences=experiences)
app = FastAPI()
service: EMService | None = None
@app.post('/agent_wrapper', response_model=AgentWrapperResponse)
def call_agent_wrapper(request: AgentWrapperRequest):
return service.call_agent_wrapper(request)
@app.post('/context_generator', response_model=ContextGeneratorResponse)
def call_context_generator(request: ContextGeneratorRequest):
return service.call_context_generator(request)
@app.post('/summarizer', response_model=SummarizerResponse)
def call_summarizer(request: SummarizerRequest):
return service.call_summarizer(request)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
field_dict = EMService.model_fields
assert isinstance(field_dict, dict)
json_keys = []
for key, info in field_dict.items():
if info.annotation in [int, str, bool]:
parser.add_argument(f"--{key}", type=info.annotation, default=info.default)
elif isinstance(info.annotation, types.UnionType) and issubclass(info.annotation.__args__[0], BaseModel):
parser.add_argument(f"--{key}", type=str, default=None)
json_keys.append(key)
elif info.annotation in [dict, list]:
logger.warning(f"skip key={key} info.annotation={info.annotation}")
continue
else:
raise NotImplementedError(f"key={key} annotation={info.annotation} is not supported.")
args: argparse.Namespace = parser.parse_args()
service_kwargs = {k: json.loads(v) if k in json_keys else v for k, v in args.__dict__.items() if v is not None}
logger.info(f"service.kwargs={json.dumps(service_kwargs, indent=2, ensure_ascii=False)}")
service = EMService(**service_kwargs)
uvicorn.run(app,
host=service.host,
port=service.port,
timeout_keep_alive=service.timeout_keep_alive,
limit_concurrency=service.limit_concurrency)
"""
launch with:
python -m experiencemaker.em_service \
--port=8001 \
--llm='{"backend": "openai_compatible", "model_name": "qwen3-32b", "temperature": 0.6}' \
--embedding_model='{"backend": "openai_compatible", "model_name": "text-embedding-v4", "dimensions": 1024}' \
--vector_store='{"backend": "elasticsearch"}' \
--agent_wrapper='{"backend": "simple"}' \
--context_generator='{"backend": "simple"}' \
--summarizer='{"backend": "simple"}'
"""

View file

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

View file

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

View file

@ -1,11 +0,0 @@
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

@ -1,2 +0,0 @@
from experiencemaker.model.openai_compatible_embedding_model import OpenAICompatibleEmbeddingModel
from experiencemaker.model.openai_compatible_llm import OpenAICompatibleBaseLLM

View file

@ -1,90 +0,0 @@
from abc import ABC
from typing import List
from loguru import logger
from pydantic import BaseModel, Field
from experiencemaker.schema.vector_store_node import VectorStoreNode
from experiencemaker.utils.registry import Registry
EMBEDDING_MODEL_REGISTRY = Registry()
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):
max_batch_size = 10 # text-embedding-v4 batch size should not be larger than 10
embeddings = [emb for i in range(0, len(nodes), max_batch_size) for emb in
self.get_embeddings(input_text=[node.content for node in nodes[i:i + max_batch_size]])]
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

@ -1,114 +0,0 @@
import time
from abc import ABC
from typing import List, Literal
from loguru import logger
from pydantic import Field, BaseModel
from experiencemaker.schema.trajectory import Message, ActionMessage
from experiencemaker.tool.base_tool import BaseTool
from experiencemaker.utils.registry import Registry
LLM_REGISTRY = Registry()
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=5, 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}")
time.sleep(1 + i)
# 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

@ -1,91 +0,0 @@
import os
from typing import Literal, List
from openai import OpenAI
from pydantic import Field, PrivateAttr, model_validator
from experiencemaker.model.base_embedding_model import BaseEmbeddingModel, EMBEDDING_MODEL_REGISTRY
@EMBEDDING_MODEL_REGISTRY.register("openai_compatible")
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-v4", 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)}")
def main():
from experiencemaker.utils.util_function import load_env_keys
load_env_keys()
model = OpenAICompatibleEmbeddingModel(dimensions=64, model_name="text-embedding-v4")
res1 = model.get_embeddings(
"The clothes are of good quality and look good, definitely worth the wait. I love them.")
res2 = model.get_embeddings(["aa", "bb"])
print(res1)
print(res2)
if __name__ == "__main__":
main()
# launch with: python -m experiencemaker.model.openai_compatible_embedding_model

View file

@ -1,3 +0,0 @@
from experiencemaker.tool.code_tool import CodeTool
from experiencemaker.tool.dashscope_search_tool import DashscopeSearchTool
from experiencemaker.tool.terminate_tool import TerminateTool

View file

@ -1,78 +0,0 @@
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")
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

@ -1,31 +0,0 @@
from typing import List
from loguru import logger
class Registry(object):
def __init__(self):
self._registry = {}
def register(self, name: str = None):
def decorator(cls):
class_name = name if name is not None else cls.__name__
if class_name in self._registry:
logger.warning(f"name={class_name} is already registered, will be overwritten.")
self._registry[class_name] = cls
return cls
return decorator
def __getitem__(self, name: str):
if name not in self._registry:
raise KeyError(f"name={name} is not registered!")
return self._registry[name]
def __contains__(self, name: str):
return name in self._registry
@property
def registered_module_names(self) -> List[str]:
return sorted(self._registry.keys())

View file

@ -17,28 +17,12 @@ class BaseEmbeddingModel(BaseModel, ABC):
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

View file

@ -1,6 +1,7 @@
import os
from typing import Literal, List
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import Field, PrivateAttr, model_validator
@ -10,46 +11,19 @@ from v1.embedding_model.base_embedding_model import BaseEmbeddingModel
@EMBEDDING_MODEL_REGISTRY.register("openai_compatible")
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-v4", description="model name")
api_key: str = Field(default_factory=lambda: os.getenv("EMBEDDING_API_KEY"), description="api key")
base_url: str = Field(default_factory=lambda: os.getenv("EMBEDDING_BASE_URL"), description="base url")
model_name: str = Field(default="", 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,
@ -57,28 +31,21 @@ class OpenAICompatibleEmbeddingModel(BaseEmbeddingModel):
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)}")
def main():
from experiencemaker.utils.util_function import load_env_keys
load_env_keys()
load_dotenv()
model = OpenAICompatibleEmbeddingModel(dimensions=64, model_name="text-embedding-v4")
res1 = model.get_embeddings(
"The clothes are of good quality and look good, definitely worth the wait. I love them.")

View file

@ -2,9 +2,10 @@ import time
from abc import ABC
from typing import List, Literal
from loguru import logger, Message
from loguru import logger
from pydantic import Field, BaseModel
from v1.schema.message import Message
from v1.tool.base_tool import BaseTool
@ -31,21 +32,18 @@ class BaseLLM(BaseModel, ABC):
def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
raise NotImplementedError
def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage:
def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message:
raise NotImplementedError
def chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage | None:
def chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message | None:
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}")
time.sleep(1 + i)
# 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

View file

@ -1,37 +1,28 @@
import os
from typing import List
from dotenv import load_dotenv
from loguru import logger
from openai import OpenAI
from openai.types import CompletionUsage
from pydantic import Field, PrivateAttr, model_validator
from experiencemaker.enumeration.chunk_enum import ChunkEnum
from experiencemaker.model.base_llm import BaseLLM, LLM_REGISTRY
from experiencemaker.schema.trajectory import Message, ActionMessage, ToolCall
from experiencemaker.tool.base_tool import BaseTool
from v1.enumeration.chunk_enum import ChunkEnum
from v1.llm import LLM_REGISTRY
from v1.llm.base_llm import BaseLLM
from v1.schema.message import Message, ToolCall
from v1.tool.base_tool import BaseTool
@LLM_REGISTRY.register("openai_compatible")
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")
model_name: str = Field(default="")
api_key: str = Field(default_factory=lambda: os.getenv("LLM_API_KEY"), description="api key")
base_url: str = Field(default_factory=lambda: os.getenv("LLM_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
@ -40,14 +31,14 @@ class OpenAICompatibleBaseLLM(BaseLLM):
try:
completion = self._client.chat.completions.create(
model=self.model_name,
messages=[x.simple_dict for x in messages],
messages=[x.simple_dump() 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,
tools=[x.simple_dump() for x in tools] if tools else None,
tool_choice=self.tool_choice,
parallel_tool_calls=self.parallel_tool_calls)
@ -103,7 +94,7 @@ class OpenAICompatibleBaseLLM(BaseLLM):
else:
yield e.args, ChunkEnum.ERROR
def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> ActionMessage:
def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message:
reasoning_content = ""
answer_content = ""
tool_calls = []
@ -118,9 +109,7 @@ class OpenAICompatibleBaseLLM(BaseLLM):
elif chunk_enum is ChunkEnum.TOOL:
tool_calls.append(chunk)
return ActionMessage(reasoning_content=reasoning_content,
content=answer_content,
tool_calls=tool_calls)
return Message(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
@ -154,14 +143,12 @@ class OpenAICompatibleBaseLLM(BaseLLM):
def main():
from experiencemaker.utils.util_function import load_env_keys
from experiencemaker.tool.dashscope_search_tool import DashscopeSearchTool
from experiencemaker.tool.code_tool import CodeTool
from experiencemaker.enumeration.role import Role
from v1.tool.dashscope_search_tool import DashscopeSearchTool
from v1.tool.code_tool import CodeTool
from v1.enumeration.role import Role
load_env_keys()
load_dotenv()
model_name = "qwen-max-2025-01-25"
# model_name = "qwen3-32b"
llm = OpenAICompatibleBaseLLM(model_name=model_name)
tools: List[BaseTool] = [DashscopeSearchTool(), CodeTool()]

View file

@ -1,12 +1,13 @@
from abc import abstractmethod, ABC
from abc import abstractmethod, ABCMeta
from loguru import logger
from v1.op.prompt_mixin import PromptMixin
from v1.pipeline.pipeline_context import PipelineContext
from v1.utils.timer import Timer
class BaseOp(ABC):
class BaseOp(PromptMixin, metaclass=ABCMeta):
def __init__(self, context: PipelineContext, **kwargs):
super().__init__(**kwargs)

66
v1/op/prompt_mixin.py Normal file
View file

@ -0,0 +1,66 @@
from pathlib import Path
import yaml
from loguru import logger
class PromptMixin:
def __init__(self, file_path: Path | str = None, prompt_dict: dict = None):
self.prompt_dict: dict = {}
self.prompt_dict.update(**self.load_prompt_by_file_path(file_path))
if prompt_dict:
for key, value in prompt_dict.items():
if isinstance(value, str):
self.prompt_dict[key] = value
logger.info(f"add prompt_dict key={key}")
@staticmethod
def load_prompt_by_file_path(file_path: Path | str = None):
prompt_dict = {}
if file_path is None:
return prompt_dict
if isinstance(file_path, str):
file_path = Path(file_path)
if not file_path.exists():
return prompt_dict
with file_path.open() as f:
prompt_dict = yaml.load(f, yaml.FullLoader)
logger.info(f"add prompt_dict keys={prompt_dict.keys()}")
return prompt_dict
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,31 @@
from pydantic import Field
from v1.schema.request import RetrieverRequest, SummarizerRequest, VectorStoreRequest, AgentRequest
from v1.schema.response import RetrieverResponse, SummarizerResponse, VectorStoreResponse, AgentResponse
from v1.utils.http_client import HttpClient
class ExperienceMakerClient(HttpClient):
base_url: str = Field(default=...)
def call_retriever(self, request: RetrieverRequest):
self.url = self.base_url + "/retriever"
return RetrieverResponse(**self.request(json_data=request.model_dump()))
def call_summarizer(self, request: SummarizerRequest):
self.url = self.base_url + "/summarizer"
return SummarizerResponse(**self.request(json_data=request.model_dump()))
def call_vector_store(self, request: VectorStoreRequest):
self.url = self.base_url + "/vector_store"
return VectorStoreResponse(**self.request(json_data=request.model_dump()))
def call_agent(self, request: AgentRequest):
self.url = self.base_url + "/agent"
return AgentResponse(**self.request(json_data=request.model_dump()))
if __name__ == "__main__":
client = ExperienceMakerClient(base_url="http://0.0.0.0:8001")
response = client.call_retriever(RetrieverRequest(workspace_id="123", query="hello world"))
print(response.model_dump())

View file

@ -19,7 +19,7 @@ class ExperienceMakerService:
self.init_app_config: AppConfig = self.config_parser.get_app_config()
self.thread_pool = ThreadPoolExecutor(max_workers=self.init_app_config.thread_pool.max_workers)
# The vectorstore is initialized at the very beginning and then used directly afterwards.
# The vectorstore is initialized at the very beginning and then used directly afterward.
self.vector_store_dict: dict = {}
for name, config in self.init_app_config.vector_store.items():
assert config.backend in VECTOR_STORE_REGISTRY, f"backend={config.backend} is not existed"

View file

@ -1,7 +1,7 @@
import sys
from io import StringIO
from experiencemaker.tool.base_tool import BaseTool
from v1.tool.base_tool import BaseTool
class CodeTool(BaseTool):

View file

@ -3,10 +3,11 @@ from typing import Literal
import dashscope
from dashscope.api_entities.dashscope_response import Message
from dotenv import load_dotenv
from loguru import logger
from pydantic import Field
from experiencemaker.tool.base_tool import BaseTool
from v1.tool.base_tool import BaseTool
class DashscopeSearchTool(BaseTool):
@ -142,8 +143,7 @@ Extract the original content related to the user's question directly from the co
def main():
from experiencemaker.utils.util_function import load_env_keys
load_env_keys()
load_dotenv()
query = "What is artificial intelligence?"
tool = DashscopeSearchTool(stream_print=True)

View file

@ -5,7 +5,7 @@ from mcp import ClientSession
from mcp.client.sse import sse_client
from pydantic import Field, model_validator
from experiencemaker.tool.base_tool import BaseTool
from v1.tool.base_tool import BaseTool
class MCPTool(BaseTool):

View file

@ -1,4 +1,4 @@
from experiencemaker.tool.base_tool import BaseTool
from v1.tool.base_tool import BaseTool
class TerminateTool(BaseTool):

View file

@ -1,17 +1,13 @@
import json
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, Field, PrivateAttr
class FileHandler(BaseModel):
file_path: str | Path = Field(default=...)
_obj: Any = PrivateAttr()
class FileHandler:
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self, file_path: str | Path):
self.file_path: Path = Path(file_path)
suffix = Path(self.file_path).suffix
if suffix == ".json":
self._obj = json

View file

@ -6,7 +6,7 @@ import requests
from loguru import logger
from pydantic import BaseModel, Field, PrivateAttr, model_validator
from experiencemaker.enumeration.http_enum import HttpEnum
from v1.enumeration.http_enum import HttpEnum
class HttpClient(BaseModel):

View file

@ -1,5 +1,4 @@
import time
from typing import Optional
from loguru import logger
@ -16,7 +15,7 @@ class Timer(object):
def __enter__(self, *args, **kwargs):
self.time_start = time.time()
logger.info(f"========== {self.name} start ==========", stacklevel=self.stack_level)
logger.info(f"---------- enter {self.name} ----------", stacklevel=self.stack_level)
return self
def __exit__(self, *args):
@ -27,7 +26,7 @@ class Timer(object):
else:
time_str = f"{self.time_cost:.3f}s"
logger.info(f"========== {self.name} end, time_cost={time_str} ==========", stacklevel=self.stack_level)
logger.info(f"---------- leave {self.name} [{time_str}] ----------", stacklevel=self.stack_level)
def timer(name: str = None, use_ms: bool = False, stack_level: int = 2):