refactor: rename package and restructure modules

- Rename experiencemaker package to reme_ai
- Move personal modules to new directory structure
- Remove unused classes and imports
- Update module initialization files
This commit is contained in:
jinli.yl 2025-08-25 23:59:08 +08:00
parent 4a2b7f9de1
commit cceebc631e
157 changed files with 3567 additions and 4242 deletions

View file

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 417 KiB

View file

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View file

Before

Width:  |  Height:  |  Size: 727 KiB

After

Width:  |  Height:  |  Size: 727 KiB

View file

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1,009 KiB

After

Width:  |  Height:  |  Size: 1,009 KiB

View file

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View file

@ -1,11 +0,0 @@
OPENAI_API_KEY=sk-xxxx
OPENAI_BASE_URL=https://xxxx/v1
EMBEDDING_API_KEY=sk-xxxx
EMBEDDING_BASE_URL=https://xxxx/v1
LLM_API_KEY=sk-xxxx
LLM_BASE_URL=https://xxxx/v1
ES_HOSTS=http://0.0.0.0:9200
DASHSCOPE_API_KEY=sk-xxxx

View file

@ -1,9 +0,0 @@
# from .app import main
__version__ = "0.1.1"
__all__ = ["main"]
# python -m build
# twine upload dist/*

View file

@ -1,52 +0,0 @@
import sys
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI
from experiencemaker.schema.request import RetrieverRequest, SummarizerRequest, VectorStoreRequest, AgentRequest
from experiencemaker.schema.response import RetrieverResponse, SummarizerResponse, VectorStoreResponse, AgentResponse
from experiencemaker.service.experience_maker_service import ExperienceMakerService
load_dotenv()
app = FastAPI()
service = ExperienceMakerService(sys.argv[1:])
@app.post('/retriever', response_model=RetrieverResponse)
def call_retriever(request: RetrieverRequest):
return service(api="retriever", request=request)
@app.post('/summarizer', response_model=SummarizerResponse)
def call_summarizer(request: SummarizerRequest):
return service(api="summarizer", request=request)
@app.post('/vector_store', response_model=VectorStoreResponse)
def call_vector_store(request: VectorStoreRequest):
return service(api="vector_store", request=request)
@app.post('/agent', response_model=AgentResponse)
def call_agent(request: AgentRequest):
return service(api="agent", request=request)
def main():
uvicorn.run(app=app,
host=service.http_service_config.host,
port=service.http_service_config.port,
timeout_keep_alive=service.http_service_config.timeout_keep_alive,
limit_concurrency=service.http_service_config.limit_concurrency)
if __name__ == "__main__":
main()
# start with:
# experiencemaker \
# http_service.port=8001 \
# llm.default.model_name=qwen3-32b \
# embedding_model.default.model_name=text-embedding-v4 \
# vector_store.default.backend=local_file

View file

@ -1,80 +0,0 @@
import json
from pathlib import Path
from loguru import logger
from omegaconf import OmegaConf, DictConfig
from experiencemaker.schema.app_config import AppConfig
class ConfigParser:
"""
Configuration parser that handles loading and merging configurations from multiple sources.
The configuration loading priority (from lowest to highest):
1. Default configuration from AppConfig schema
2. YAML configuration file
3. Command line arguments
4. Runtime keyword arguments
"""
def __init__(self, args: list):
"""
Initialize the configuration parser with command line arguments.
Args:
args: List of command line arguments in dotlist format (e.g., ['key=value'])
"""
# Step 1: Initialize with default configuration from AppConfig schema
self.app_config: DictConfig = OmegaConf.structured(AppConfig)
# Step 2: Load configuration from YAML file
# First, parse CLI arguments to check if custom config path is specified
cli_config: DictConfig = OmegaConf.from_dotlist(args)
temp_config: AppConfig = OmegaConf.to_object(OmegaConf.merge(self.app_config, cli_config))
# Determine config file path: either from CLI args or use predefined config
if temp_config.config_path:
# Use custom config path if provided
config_path = Path(temp_config.config_path)
else:
# Use predefined config name from the config directory
pre_defined_config = temp_config.pre_defined_config
if not pre_defined_config.endswith(".yaml"):
pre_defined_config += ".yaml"
config_path = Path(__file__).parent / pre_defined_config
logger.info(f"load config from path={config_path}")
yaml_config = OmegaConf.load(config_path)
# Merge YAML config with default config
self.app_config = OmegaConf.merge(self.app_config, yaml_config)
# Step 3: Merge CLI arguments (highest priority)
self.app_config = OmegaConf.merge(self.app_config, cli_config)
# Log the final merged configuration
app_config_dict = OmegaConf.to_container(self.app_config, resolve=True)
logger.info(f"app_config=\n{json.dumps(app_config_dict, indent=2, ensure_ascii=False)}")
def get_app_config(self, **kwargs) -> AppConfig:
"""
Get the application configuration with optional runtime overrides.
Args:
**kwargs: Additional configuration parameters to override at runtime
Returns:
AppConfig: The final application configuration object
"""
# Create a copy of the current configuration
app_config = self.app_config.copy()
# Apply runtime overrides if provided
if kwargs:
# Convert kwargs to dotlist format for OmegaConf
kwargs_list = [f"{k}={v}" for k, v in kwargs.items()]
update_config = OmegaConf.from_dotlist(kwargs_list)
app_config = OmegaConf.merge(app_config, update_config)
# Convert OmegaConf DictConfig to structured AppConfig object
return OmegaConf.to_object(app_config)

View file

@ -1,58 +0,0 @@
# demo config.yaml
http_service:
host: "0.0.0.0"
port: 8001
timeout_keep_alive: 600
limit_concurrency: 64
thread_pool:
max_workers: 10
api:
retriever: mock1_op->[mock4_op->mock2_op|mock5_op]->[mock3_op|mock6_op]
summarizer: mock1_op->[mock4_op->mock2_op|mock5_op]->mock3_op
vector_store: mock6_op
op:
mock1_op:
backend: mock1_op
llm: default
vector_store: default
params:
a: 1
b: 2
mock2_op:
backend: mock2_op
params:
a: 1
mock3_op:
backend: mock3_op
mock4_op:
backend: mock4_op
mock5_op:
backend: mock5_op
mock6_op:
backend: mock6_op
llm:
default:
backend: openai_compatible
model_name: qwen3-32b
params:
temperature: 0.6
embedding_model:
default:
backend: openai_compatible
model_name: text-embedding-v4
params:
dimensions: 1024
vector_store:
default:
backend: elasticsearch
embedding_model: default
params:
hosts: "http://localhost:9200"

View file

@ -1,5 +0,0 @@
from experiencemaker.utils.registry import Registry
EMBEDDING_MODEL_REGISTRY = Registry()
from experiencemaker.embedding_model.openai_compatible_embedding_model import OpenAICompatibleEmbeddingModel

View file

@ -1,103 +0,0 @@
from abc import ABC
from typing import List
from loguru import logger
from pydantic import BaseModel, Field
from experiencemaker.schema.vector_node import VectorNode
class BaseEmbeddingModel(BaseModel, ABC):
"""
Abstract base class for embedding models.
This class provides a common interface for various embedding model implementations,
including retry logic, error handling, and batch processing capabilities.
"""
# Model configuration fields
model_name: str = Field(default=..., description="Name of the embedding model")
dimensions: int = Field(default=..., description="Dimensionality of the embedding vectors")
max_retries: int = Field(default=3, description="Maximum number of retry attempts on failure")
raise_exception: bool = Field(default=True, description="Whether to raise exceptions after max retries")
max_batch_size: int = Field(default=10, description="Maximum batch size for processing (text-embedding-v4 should not exceed 10)")
def _get_embeddings(self, input_text: str | List[str]):
"""
Abstract method to get embeddings from the model.
This method must be implemented by concrete subclasses to provide
the actual embedding functionality.
Args:
input_text: Single text string or list of text strings to embed
Returns:
Embedding vector(s) corresponding to the input text(s)
"""
raise NotImplementedError
def get_embeddings(self, input_text: str | List[str]):
"""
Get embeddings with retry logic and error handling.
This method wraps the _get_embeddings method with automatic retry
functionality in case of failures.
Args:
input_text: Single text string or list of text strings to embed
Returns:
Embedding vector(s) or None if all retries failed and raise_exception is False
"""
# Retry loop with exponential backoff potential
for i in range(self.max_retries):
try:
return self._get_embeddings(input_text)
except Exception as e:
logger.exception(f"embedding model name={self.model_name} encounter error with e={e.args}")
# If this is the last retry and raise_exception is True, re-raise the exception
if i == self.max_retries - 1 and self.raise_exception:
raise e
# Return None if all retries failed and raise_exception is False
return None
def get_node_embeddings(self, nodes: VectorNode | List[VectorNode]):
"""
Generate embeddings for VectorNode objects and update their vector fields.
This method handles both single nodes and lists of nodes, with automatic
batching for efficient processing of large node lists.
Args:
nodes: Single VectorNode or list of VectorNode objects to embed
Returns:
The same node(s) with updated vector fields containing embeddings
Raises:
RuntimeError: If unsupported node type is provided
"""
# Handle single VectorNode
if isinstance(nodes, VectorNode):
nodes.vector = self.get_embeddings(nodes.content)
return nodes
# Handle list of VectorNodes with batch processing
elif isinstance(nodes, list):
# Process nodes in batches to respect max_batch_size limits
embeddings = [emb for i in range(0, len(nodes), self.max_batch_size) for emb in
self.get_embeddings(input_text=[node.content for node in nodes[i:i + self.max_batch_size]])]
# Validate that we got the expected number of embeddings
if len(embeddings) != len(nodes):
logger.warning(f"embeddings.size={len(embeddings)} <> nodes.size={len(nodes)}")
else:
# Assign embeddings to corresponding nodes
for node, embedding in zip(nodes, embeddings):
node.vector = embedding
return nodes
else:
raise RuntimeError(f"unsupported type={type(nodes)}")

View file

@ -1,93 +0,0 @@
import os
from typing import Literal, List
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import Field, PrivateAttr, model_validator
from experiencemaker.embedding_model import EMBEDDING_MODEL_REGISTRY
from experiencemaker.embedding_model.base_embedding_model import BaseEmbeddingModel
@EMBEDDING_MODEL_REGISTRY.register("openai_compatible")
class OpenAICompatibleEmbeddingModel(BaseEmbeddingModel):
"""
OpenAI-compatible embedding model implementation.
This class provides an implementation of BaseEmbeddingModel that works with
OpenAI-compatible embedding APIs, including OpenAI's official API and
other services that follow the same interface.
"""
# API configuration fields
api_key: str = Field(default_factory=lambda: os.getenv("EMBEDDING_API_KEY"), description="API key for authentication")
base_url: str = Field(default_factory=lambda: os.getenv("EMBEDDING_BASE_URL"), description="Base URL for the API endpoint")
model_name: str = Field(default="", description="Name of the embedding model to use")
dimensions: int = Field(default=1024, description="Dimensionality of the embedding vectors")
encoding_format: Literal["float", "base64"] = Field(default="float", description="Encoding format for embeddings")
# Private OpenAI client instance
_client: OpenAI = PrivateAttr()
@model_validator(mode="after")
def init_client(self):
"""
Initialize the OpenAI client after model validation.
This method is called automatically after Pydantic model validation
to set up the OpenAI client with the provided API key and base URL.
Returns:
self: The model instance 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]):
"""
Get embeddings from the OpenAI-compatible API.
This method implements the abstract _get_embeddings method from BaseEmbeddingModel
by calling the OpenAI-compatible embeddings API.
Args:
input_text: Single text string or list of text strings to embed
Returns:
Embedding vector(s) corresponding to the input text(s)
Raises:
RuntimeError: If unsupported input type is provided
"""
completion = self._client.embeddings.create(
model=self.model_name,
input=input_text,
dimensions=self.dimensions,
encoding_format=self.encoding_format
)
if isinstance(input_text, str):
return completion.data[0].embedding
elif isinstance(input_text, list):
result_emb = [[] for _ in range(len(input_text))]
for emb in completion.data:
result_emb[emb.index] = emb.embedding
return result_emb
else:
raise RuntimeError(f"unsupported type={type(input_text)}")
def main():
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.")
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,8 +0,0 @@
from enum import Enum
class AgentState(str, Enum):
IDLE = "idle"
RUNNING = "running"
COMPLETE = "complete"
FAILED = "failed"

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,9 +0,0 @@
from enum import Enum
class HttpEnum(str, Enum):
GET = "get"
POST = "post"
HEAD = "head"
PUT = "put"
DELETE = "delete"

View file

@ -1,8 +0,0 @@
from enum import Enum
class Role(str, Enum):
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
TOOL = "tool"

View file

@ -1,5 +0,0 @@
from experiencemaker.utils.registry import Registry
LLM_REGISTRY = Registry()
from experiencemaker.llm.openai_compatible_llm import OpenAICompatibleBaseLLM

View file

@ -1,138 +0,0 @@
import time
from abc import ABC
from typing import List, Literal, Callable
from loguru import logger
from pydantic import Field, BaseModel
from experiencemaker.schema.message import Message
from experiencemaker.tool.base_tool import BaseTool
class BaseLLM(BaseModel, ABC):
"""
Abstract base class for Large Language Model (LLM) implementations.
This class defines the common interface and configuration parameters
that all LLM implementations should support. It provides a standardized
way to interact with different LLM providers while handling common
concerns like retries, error handling, and streaming.
"""
# Core model configuration
model_name: str = Field(..., description="Name of the LLM model to use")
# Generation parameters
seed: int = Field(default=42, description="Random seed for reproducible outputs")
top_p: float | None = Field(default=None, description="Top-p (nucleus) sampling parameter")
# stream: bool = Field(default=True) # Commented out - streaming is handled per request
stream_options: dict = Field(default={"include_usage": True}, description="Options for streaming responses")
temperature: float = Field(default=0.0000001, description="Sampling temperature (low for deterministic outputs)")
presence_penalty: float | None = Field(default=None, description="Presence penalty to reduce repetition")
# Model-specific features
enable_thinking: bool = Field(default=True, description="Enable reasoning/thinking mode for supported models")
# Tool usage configuration
tool_choice: Literal["none", "auto", "required"] = Field(default="auto", description="Strategy for tool selection")
parallel_tool_calls: bool = Field(default=True, description="Allow multiple tool calls in parallel")
# Error handling and reliability
max_retries: int = Field(default=5, description="Maximum number of retry attempts on failure")
raise_exception: bool = Field(default=False, description="Whether to raise exceptions or return default values")
def stream_chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
"""
Stream chat completions from the LLM.
This method should yield chunks of the response as they become available,
allowing for real-time display of the model's output.
Args:
messages: List of conversation messages
tools: Optional list of tools the model can use
**kwargs: Additional model-specific parameters
Yields:
Chunks of the streaming response with their types
"""
raise NotImplementedError
def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
"""
Stream chat completions and print them to console in real-time.
This is a convenience method for debugging and interactive use,
combining streaming with formatted console output.
Args:
messages: List of conversation messages
tools: Optional list of tools the model can use
**kwargs: Additional model-specific parameters
"""
raise NotImplementedError
def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message:
"""
Internal method to perform a single chat completion.
This method should be implemented by subclasses to handle the actual
communication with the LLM provider. It's called by the public chat()
method which adds retry logic and error handling.
Args:
messages: List of conversation messages
tools: Optional list of tools the model can use
**kwargs: Additional model-specific parameters
Returns:
The complete response message from the LLM
"""
raise NotImplementedError
def chat(self, messages: List[Message], tools: List[BaseTool] = None, callback_fn: Callable = None,
default_value=None, **kwargs):
"""
Perform a chat completion with retry logic and error handling.
This is the main public interface for chat completions. It wraps the
internal _chat() method with robust error handling, exponential backoff,
and optional callback processing.
Args:
messages: List of conversation messages
tools: Optional list of tools the model can use
callback_fn: Optional callback to process the response message
default_value: Value to return if all retries fail (when raise_exception=False)
**kwargs: Additional model-specific parameters
Returns:
The response message (possibly processed by callback_fn) or default_value
Raises:
Exception: If raise_exception=True and all retries fail
"""
for i in range(self.max_retries):
try:
# Attempt to get response from the model
message: Message = self._chat(messages, tools, **kwargs)
# Apply callback function if provided
if callback_fn:
return callback_fn(message)
else:
return message
except Exception as e:
logger.exception(f"chat with model={self.model_name} encounter error with e={e.args}")
# Exponential backoff: wait longer after each failure
time.sleep(1 + i)
# Handle final retry failure
if i == self.max_retries - 1:
if self.raise_exception:
raise e
else:
return default_value
return None

View file

@ -1,282 +0,0 @@
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.enumeration.role import Role
from experiencemaker.llm import LLM_REGISTRY
from experiencemaker.llm.base_llm import BaseLLM
from experiencemaker.schema.message import Message, ToolCall
from experiencemaker.tool.base_tool import BaseTool
@LLM_REGISTRY.register("openai_compatible")
class OpenAICompatibleBaseLLM(BaseLLM):
"""
OpenAI-compatible LLM implementation supporting streaming and tool calls.
This class implements the BaseLLM interface for OpenAI-compatible APIs,
including support for:
- Streaming responses with different chunk types (thinking, answer, tools)
- Tool calling with parallel execution
- Reasoning/thinking content from supported models
- Robust error handling and retries
"""
# API configuration
api_key: str = Field(default_factory=lambda: os.getenv("LLM_API_KEY"), description="API key for authentication")
base_url: str = Field(default_factory=lambda: os.getenv("LLM_BASE_URL"), description="Base URL for the API endpoint")
_client: OpenAI = PrivateAttr()
@model_validator(mode="after")
def init_client(self):
"""
Initialize the OpenAI client after model validation.
This validator runs after all field validation is complete,
ensuring we have valid API credentials before creating the client.
Returns:
Self 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):
"""
Stream chat completions from OpenAI-compatible API.
This method handles streaming responses and categorizes chunks into different types:
- THINK: Reasoning/thinking content from the model
- ANSWER: Regular response content
- TOOL: Tool calls that need to be executed
- USAGE: Token usage statistics
- ERROR: Error information
Args:
messages: List of conversation messages
tools: Optional list of tools available to the model
**kwargs: Additional parameters
Yields:
Tuple of (chunk_content, ChunkEnum) for each streaming piece
"""
for i in range(self.max_retries):
try:
# Create streaming completion request
completion = self._client.chat.completions.create(
model=self.model_name,
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}, # Enable reasoning mode
tools=[x.simple_dump() for x in tools] if tools else None,
tool_choice=self.tool_choice,
parallel_tool_calls=self.parallel_tool_calls)
# Initialize tool call tracking
ret_tools = [] # Accumulate tool calls across chunks
is_answering = False # Track when model starts answering
# Process each chunk in the streaming response
for chunk in completion:
# Handle chunks without choices (usually usage info)
if not chunk.choices:
yield chunk.usage, ChunkEnum.USAGE
else:
delta = chunk.choices[0].delta
# Handle reasoning/thinking content (model's internal thoughts)
if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
yield delta.reasoning_content, ChunkEnum.THINK
else:
# Mark transition from thinking to answering
if not is_answering:
is_answering = True
# Handle regular response content
if delta.content is not None:
yield delta.content, ChunkEnum.ANSWER
# Handle tool calls (function calling)
if delta.tool_calls is not None:
for tool_call in delta.tool_calls:
index = tool_call.index
# Ensure we have enough tool call slots
while len(ret_tools) <= index:
ret_tools.append(ToolCall(index=index))
# Accumulate tool call information across chunks
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
# Yield completed tool calls after streaming finishes
if ret_tools:
tool_dict = {x.name: x for x in tools} if tools else {}
for tool in ret_tools:
# Only yield tool calls that correspond to available tools
if tool.name not in tool_dict:
continue
yield tool, ChunkEnum.TOOL
return # Success - exit retry loop
except Exception as e:
logger.exception(f"stream chat with model={self.model_name} encounter error with e={e.args}")
# Handle retry logic
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) -> Message:
"""
Perform a complete chat completion by aggregating streaming chunks.
This method consumes the entire streaming response and combines all
chunks into a single Message object. It separates reasoning content,
regular answer content, and tool calls.
Args:
messages: List of conversation messages
tools: Optional list of tools available to the model
**kwargs: Additional parameters
Returns:
Complete Message with all content aggregated
"""
# Initialize content accumulators
reasoning_content = "" # Model's internal reasoning
answer_content = "" # Final response content
tool_calls = [] # List of tool calls to execute
# Consume streaming response and aggregate chunks by type
for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
if 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)
# Note: USAGE and ERROR chunks are ignored in non-streaming mode
# Construct complete response message
return Message(role=Role.ASSISTANT,
reasoning_content=reasoning_content,
content=answer_content,
tool_calls=tool_calls)
def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
"""
Stream chat completions with formatted console output.
This method provides a real-time view of the model's response,
with different formatting for different types of content:
- Thinking content is wrapped in <think></think> tags
- Answer content is printed directly
- Tool calls are formatted as JSON
- Usage statistics and errors are clearly marked
Args:
messages: List of conversation messages
tools: Optional list of tools available to the model
**kwargs: Additional parameters
"""
# Track which sections we've entered for proper formatting
enter_think = False # Whether we've started printing thinking content
enter_answer = False # Whether we've started printing answer content
# Process each streaming chunk with appropriate formatting
for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
if chunk_enum is ChunkEnum.USAGE:
# Display token usage statistics
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:
# Format thinking/reasoning content
if not enter_think:
enter_think = True
print("<think>\n", end="")
print(chunk, end="")
elif chunk_enum is ChunkEnum.ANSWER:
# Format regular answer content
if not enter_answer:
enter_answer = True
# Close thinking section if we were in it
if enter_think:
print("\n</think>")
print(chunk, end="")
elif chunk_enum is ChunkEnum.TOOL:
# Format tool calls as structured JSON
assert isinstance(chunk, ToolCall)
print(f"\n<tool>{chunk.model_dump_json(indent=2)}</tool>", end="")
elif chunk_enum is ChunkEnum.ERROR:
# Display error information
print(f"\n<error>{chunk}</error>", end="")
def main():
"""
Demo function to test the OpenAI-compatible LLM implementation.
This function demonstrates:
1. Basic chat without tools
2. Chat with tool usage (search and code tools)
3. Real-time streaming output formatting
"""
from experiencemaker.tool.dashscope_search_tool import DashscopeSearchTool
from experiencemaker.tool.code_tool import CodeTool
from experiencemaker.enumeration.role import Role
# Load environment variables for API credentials
load_dotenv()
# Initialize the LLM with a specific model
model_name = "qwen-max-2025-01-25"
llm = OpenAICompatibleBaseLLM(model_name=model_name)
# Set up available tools
tools: List[BaseTool] = [DashscopeSearchTool(), CodeTool()]
# Test 1: Simple greeting without tools
print("=== Test 1: Simple Chat ===")
llm.stream_print([Message(role=Role.USER, content="hello")], [])
print("\n" + "=" * 20)
# Test 2: Complex query that might use tools
print("\n=== Test 2: Chat with Tools ===")
llm.stream_print([Message(role=Role.USER, content="What's the weather like in Beijing today?")], tools)
if __name__ == "__main__":
main()
# Launch with: python -m experiencemaker.llm.openai_compatible_llm

View file

@ -1,111 +0,0 @@
import sys
from typing import List
from dotenv import load_dotenv
from fastmcp import FastMCP
from experiencemaker.service.experience_maker_service import ExperienceMakerService
load_dotenv()
mcp = FastMCP("ExperienceMaker")
service = ExperienceMakerService(sys.argv[1:])
@mcp.tool
def retriever(query: str,
messages: List[dict] = None,
top_k: int = 1,
workspace_id: str = "default",
config: dict = None) -> dict:
"""
Retrieve experiences from the workspace based on a query.
Args:
query: Query string
messages: List of messages
top_k: Number of top experiences to retrieve
workspace_id: Workspace identifier
config: Additional configuration parameters
Returns:
Dictionary containing retrieved experiences
"""
return service(api="retriever", request={
"query": query,
"messages": messages if messages else [],
"top_k": top_k,
"workspace_id": workspace_id,
"config": config if config else {},
}).model_dump()
@mcp.tool
def summarizer(traj_list: List[dict], workspace_id: str = "default", config: dict = None) -> dict:
"""
Summarize trajectories into experiences.
Args:
traj_list: List of trajectories
workspace_id: Workspace identifier
config: Additional configuration parameters
Returns:
experiences
"""
return service(api="summarizer", request={
"traj_list": traj_list,
"workspace_id": workspace_id,
"config": config if config else {},
}).model_dump()
@mcp.tool
def vector_store(action: str,
src_workspace_id: str = "",
workspace_id: str = "",
path: str = "./",
config: dict = None) -> dict:
"""
Perform vector store operations.
Args:
action: Action to perform (e.g., "copy", "delete", "dump", "load")
src_workspace_id: Source workspace identifier
workspace_id: Workspace identifier
path: Path to the vector store
config: Additional configuration parameters
Returns:
Dictionary containing the result of the vector store operation
"""
return service(api="vector_store", request={
"action": action,
"src_workspace_id": src_workspace_id,
"workspace_id": workspace_id,
"path": path,
"config": config if config else {},
}).model_dump()
def main():
mcp_transport: str = service.init_app_config.mcp_transport
if mcp_transport == "sse":
mcp.run(transport="sse", host=service.http_service_config.host, port=service.http_service_config.port)
elif mcp_transport == "stdio":
mcp.run(transport="stdio")
else:
raise ValueError(f"Unsupported mcp transport: {mcp_transport}")
if __name__ == "__main__":
main()
# start with:
# experiencemaker_mcp \
# mcp_transport=stdio \
# http_service.port=8001 \
# llm.default.model_name=qwen3-32b \
# embedding_model.default.model_name=text-embedding-v4 \
# vector_store.default.backend=local_file

View file

@ -1,26 +0,0 @@
from experiencemaker.utils.registry import Registry
OP_REGISTRY = Registry()
from experiencemaker.op.mock_op import Mock1Op, Mock2Op, Mock3Op, Mock4Op, Mock5Op, Mock6Op
from experiencemaker.op.retriever.build_query_op import BuildQueryOp
from experiencemaker.op.retriever.merge_experience_op import MergeExperienceOp
from experiencemaker.op.summarizer.simple_summary_op import SimpleSummaryOp
from experiencemaker.op.summarizer.trajectory_preprocess_op import TrajectoryPreprocessOp
from experiencemaker.op.summarizer.comparative_extraction_op import ComparativeExtractionOp
from experiencemaker.op.summarizer.success_extraction_op import SuccessExtractionOp
from experiencemaker.op.summarizer.failure_extraction_op import FailureExtractionOp
from experiencemaker.op.summarizer.experience_validation_op import ExperienceValidationOp
from experiencemaker.op.summarizer.experience_deduplication_op import ExperienceDeduplicationOp
from experiencemaker.op.summarizer.experience_validation_op import ExperienceValidationOp
from experiencemaker.op.summarizer.trajectory_segmentation_op import TrajectorySegmentationOp
from experiencemaker.op.summarizer.simple_comparative_summary_op import SimpleComparativeSummaryOp
from experiencemaker.op.retriever.rerank_experience_op import RerankExperienceOp
from experiencemaker.op.retriever.rewrite_experience_op import RewriteExperienceOp
from experiencemaker.op.vector_store.update_vector_store_op import UpdateVectorStoreOp
from experiencemaker.op.vector_store.recall_vector_store_op import RecallVectorStoreOp
from experiencemaker.op.vector_store.vector_store_action_op import VectorStoreActionOp
from experiencemaker.op.react.react_v1_op import ReactV1Op

View file

@ -1,125 +0,0 @@
from abc import abstractmethod, ABC
from concurrent.futures import Future
from pathlib import Path
from typing import List
from loguru import logger
from tqdm import tqdm
from experiencemaker.embedding_model import EMBEDDING_MODEL_REGISTRY
from experiencemaker.embedding_model.base_embedding_model import BaseEmbeddingModel
from experiencemaker.llm import LLM_REGISTRY
from experiencemaker.llm.base_llm import BaseLLM
from experiencemaker.op.prompt_mixin import PromptMixin
from experiencemaker.pipeline.pipeline_context import PipelineContext
from experiencemaker.schema.app_config import OpConfig, LLMConfig, EmbeddingModelConfig
from experiencemaker.utils.common_utils import camel_to_snake
from experiencemaker.utils.timer import Timer
from experiencemaker.vector_store.base_vector_store import BaseVectorStore
class BaseOp(PromptMixin, ABC):
current_path: str = __file__
def __init__(self, context: PipelineContext, op_config: OpConfig):
super().__init__()
self.context: PipelineContext = context
self.op_config: OpConfig = op_config
self.timer = Timer(name=self.simple_name)
self._prepare_prompt()
self._llm: BaseLLM | None = None
self._embedding_model: BaseEmbeddingModel | None = None
self._vector_store: BaseVectorStore | None = None
self.task_list: List[Future] = []
def _prepare_prompt(self):
if self.op_config.prompt_file_path:
prompt_file_path = self.op_config.prompt_file_path
else:
prompt_name = self.simple_name.replace("_op", "_prompt.yaml")
prompt_file_path = Path(self.current_path).parent / prompt_name
# Load custom prompts from prompt file
self.load_prompt_by_file(prompt_file_path=prompt_file_path)
# Load custom prompts from config
self.load_prompt_dict(prompt_dict=self.op_config.prompt_dict)
@property
def simple_name(self) -> str:
return camel_to_snake(self.__class__.__name__)
@property
def op_params(self) -> dict:
return self.op_config.params
@abstractmethod
def execute(self):
...
def execute_wrap(self):
try:
with self.timer:
return self.execute()
except Exception as e:
logger.exception(f"op={self.simple_name} execute failed, error={e.args}")
def submit_task(self, fn, *args, **kwargs):
task = self.context.thread_pool.submit(fn, *args, **kwargs)
self.task_list.append(task)
return self
def join_task(self, task_desc: str = None) -> list:
result = []
for task in tqdm(self.task_list, desc=task_desc or (self.simple_name + ".join_task")):
t_result = task.result()
if t_result:
if isinstance(t_result, list):
result.extend(t_result)
else:
result.append(t_result)
self.task_list.clear()
return result
@property
def llm(self) -> BaseLLM:
if self._llm is None:
llm_name: str = self.op_config.llm
assert llm_name in self.context.app_config.llm, f"llm={llm_name} not found in app_config.llm!"
llm_config: LLMConfig = self.context.app_config.llm[llm_name]
assert llm_config.backend in LLM_REGISTRY, f"llm.backend={llm_config.backend} not found in LLM_REGISTRY!"
llm_cls = LLM_REGISTRY[llm_config.backend]
self._llm = llm_cls(model_name=llm_config.model_name, **llm_config.params)
return self._llm
@property
def embedding_model(self):
if self._embedding_model is None:
embedding_model_name: str = self.op_config.embedding_model
assert embedding_model_name in self.context.app_config.embedding_model, \
f"embedding_model={embedding_model_name} not found in app_config.embedding_model!"
embedding_model_config: EmbeddingModelConfig = self.context.app_config.embedding_model[embedding_model_name]
assert embedding_model_config.backend in EMBEDDING_MODEL_REGISTRY, \
f"embedding_model.backend={embedding_model_config.backend} not found in EMBEDDING_MODEL_REGISTRY!"
embedding_model_cls = EMBEDDING_MODEL_REGISTRY[embedding_model_config.backend]
self._embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
**embedding_model_config.params)
return self._embedding_model
@property
def vector_store(self):
if self._vector_store is None:
vector_store_name: str = self.op_config.vector_store
assert vector_store_name in self.context.vector_store_dict, \
f"vector_store={vector_store_name} not found in vector_store_dict!"
self._vector_store = self.context.vector_store_dict[vector_store_name]
return self._vector_store

View file

@ -1,40 +0,0 @@
import time
from loguru import logger
from experiencemaker.op import OP_REGISTRY
from experiencemaker.op.base_op import BaseOp
@OP_REGISTRY.register()
class Mock1Op(BaseOp):
def execute(self):
time.sleep(1)
a: int = self.op_params["a"]
b: str = self.op_params["b"]
logger.info(f"enter class={self.simple_name}. a={a} b={b}")
@OP_REGISTRY.register()
class Mock2Op(Mock1Op):
...
@OP_REGISTRY.register()
class Mock3Op(Mock1Op):
...
@OP_REGISTRY.register()
class Mock4Op(Mock1Op):
...
@OP_REGISTRY.register()
class Mock5Op(Mock1Op):
...
@OP_REGISTRY.register()
class Mock6Op(Mock1Op):
...

View file

@ -1,74 +0,0 @@
from pathlib import Path
import yaml
from loguru import logger
class PromptMixin:
def __init__(self):
self._prompt_dict: dict = {}
def load_prompt_by_file(self, prompt_file_path: Path | str = None):
if prompt_file_path is None:
return
if isinstance(prompt_file_path, str):
prompt_file_path = Path(prompt_file_path)
if not prompt_file_path.exists():
return
with prompt_file_path.open() as f:
prompt_dict = yaml.load(f, yaml.FullLoader)
self.load_prompt_dict(prompt_dict)
def load_prompt_dict(self, prompt_dict: dict = None):
if not prompt_dict:
return
for key, value in prompt_dict.items():
if isinstance(value, str):
if key in self._prompt_dict:
self._prompt_dict[key] = value
logger.warning(f"prompt_dict key={key} overwrite!")
else:
self._prompt_dict[key] = value
logger.info(f"add prompt_dict key={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
def get_prompt(self, key: str):
return self._prompt_dict[key]

View file

@ -1,42 +0,0 @@
from loguru import logger
from experiencemaker.op import OP_REGISTRY
from experiencemaker.op.base_op import BaseOp
from experiencemaker.schema.request import RetrieverRequest
from experiencemaker.utils.op_utils import merge_messages_content
@OP_REGISTRY.register()
class BuildQueryOp(BaseOp):
current_path: str = __file__
def execute(self):
request: RetrieverRequest = self.context.request
if request.query:
query = request.query
elif request.messages:
enable_llm_build: str = str(self.op_params.get("enable_llm_build"))
if enable_llm_build and enable_llm_build.lower() == "true":
execution_process = merge_messages_content(request.messages)
query = self.prompt_format(prompt_name="query_build", execution_process=execution_process)
else:
context_parts = []
message_summaries = []
for message in request.messages[-3:]: # Last 3 messages
content = message.content[:200] + "..." if len(message.content) > 200 else message.content
message_summaries.append(f"- {message.role.value}: {content}")
if message_summaries:
context_parts.append("Recent messages:\n" + "\n".join(message_summaries))
query = "\n\n".join(context_parts)
else:
raise RuntimeError("query or messages is required!")
logger.info(f"build.query={query}")
from experiencemaker.op.vector_store.recall_vector_store_op import RecallVectorStoreOp
self.context.set_context(RecallVectorStoreOp.SEARCH_QUERY, query)
self.context.set_context(RecallVectorStoreOp.SEARCH_MESSAGE, request.messages)

View file

@ -1,6 +0,0 @@
query_build: |
# Execution Process
{execution_process}
Read through the entire execution process to understand which part is currently being executed.
Generate a `query` that reflects the current state, which will later be used to search for similar problems in the database and help resolve the issue at hand.

View file

@ -1,31 +0,0 @@
from typing import List
from loguru import logger
from experiencemaker.op import OP_REGISTRY
from experiencemaker.op.base_op import BaseOp
from experiencemaker.schema.experience import BaseExperience
from experiencemaker.schema.response import RetrieverResponse
@OP_REGISTRY.register()
class MergeExperienceOp(BaseOp):
def execute(self):
response: RetrieverResponse = self.context.response
experience_list: List[BaseExperience] = response.experience_list
if not experience_list:
return
content_collector = ["Previous Experience"]
for experience in experience_list:
if not experience.content:
continue
content_collector.append(f"- when_to_use: {experience.when_to_use}\n"
f"content: {experience.content}\n")
content_collector.append("Please consider the helpful parts from these in answering the question, "
"to make the response more comprehensive and substantial.")
response.experience_merged = "\n".join(content_collector)
logger.info(f"experience_merged={response.experience_merged}")

View file

@ -1,13 +0,0 @@
"""
1. retrieve:
search: query(context), workspace_id(request), top_k(request)
2. summary:
insert: nodes(context), workspace_id(request)
delete: ids(context), workspace_id(request)
search: query(context), workspace_id(request), top_k(request.config.op)
3. vector:
dump: workspace_id(request), path(str), max_size(int)
load: workspace_id(request), path(str)
delete: workspace_id(request)
copy: source_id, target_id, max_size(int)
"""

View file

@ -1,48 +0,0 @@
from typing import List
from loguru import logger
from experiencemaker.op import OP_REGISTRY
from experiencemaker.op.base_op import BaseOp
from experiencemaker.schema.experience import BaseExperience, vector_node_to_experience
from experiencemaker.schema.request import RetrieverRequest
from experiencemaker.schema.response import RetrieverResponse
from experiencemaker.schema.vector_node import VectorNode
@OP_REGISTRY.register()
class RecallVectorStoreOp(BaseOp):
SEARCH_QUERY = "search_query"
SEARCH_MESSAGE = "search_message"
def execute(self):
# get query
query = self.context.get_context(self.SEARCH_QUERY)
assert query, "query should be not empty!"
# retrieve from vector store
request: RetrieverRequest = self.context.request
nodes: List[VectorNode] = self.vector_store.search(query=query,
workspace_id=request.workspace_id,
top_k=request.top_k)
# convert to experience, filter duplicate
experience_list: List[BaseExperience] = []
experience_content_list: List[str] = []
for node in nodes:
experience: BaseExperience = vector_node_to_experience(node)
if experience.content not in experience_content_list:
experience_list.append(experience)
experience_content_list.append(experience.content)
experience_size = len(experience_list)
logger.info(f"retrieve experience size={experience_size}")
# filter by score
threshold_score: float | None = self.op_params.get("threshold_score", None)
if threshold_score is not None:
experience_list = [e for e in experience_list if e.score >= threshold_score or e.score is None]
logger.info(f"after filter by threshold_score size={len(experience_list)}")
# set response
response: RetrieverResponse = self.context.response
response.experience_list = experience_list

View file

@ -1,94 +0,0 @@
from concurrent.futures import as_completed
from itertools import zip_longest
from typing import List
from loguru import logger
from experiencemaker.op import OP_REGISTRY
from experiencemaker.op.base_op import BaseOp
from experiencemaker.pipeline.pipeline_context import PipelineContext
from experiencemaker.utils.timer import Timer, timer
class Pipeline:
seq_symbol: str = "->"
parallel_symbol: str = "|"
def __init__(self, pipeline: str, context: PipelineContext):
self.pipeline_list: List[str | List[str]] = self._parse_pipline(pipeline)
self.context: PipelineContext = context
def _parse_pipline(self, pipeline: str) -> List[str | List[str]]:
pipeline_list: List[str | List[str]] = []
for pipeline_split1 in pipeline.split("["):
for sub_pipeline in pipeline_split1.split("]"):
sub_pipeline = sub_pipeline.strip().strip(self.seq_symbol)
if not sub_pipeline:
continue
if self.parallel_symbol in sub_pipeline:
pipeline_list.append(sub_pipeline.split(self.parallel_symbol))
else:
pipeline_list.append(sub_pipeline)
logger.info(f"add sub_pipeline={sub_pipeline}")
return pipeline_list
def _execute_sub_pipeline(self, pipeline: str):
op_config_dict = self.context.app_config.op
for op in pipeline.split(self.seq_symbol):
op = op.strip()
if not op:
continue
assert op in op_config_dict, f"op={op} config is missing!"
op_config = op_config_dict[op]
assert op_config.backend in OP_REGISTRY, f"op={op} backend={op_config.backend} is not registered!"
op_cls = OP_REGISTRY[op_config.backend]
op_obj: BaseOp = op_cls(context=self.context, op_config=op_config)
op_obj.execute_wrap()
def _parse_sub_pipeline(self, pipeline: str):
for op in pipeline.split(self.seq_symbol):
op = op.strip()
if not op:
continue
yield op
def print_pipeline(self):
i: int = 0
for pipeline in self.pipeline_list:
if isinstance(pipeline, str):
for op in self._parse_sub_pipeline(pipeline):
i += 1
logger.info(f"stage_{i}: {op}")
elif isinstance(pipeline, list):
parallel_pipeline = [self._parse_sub_pipeline(x) for x in pipeline]
for op_list in zip_longest(*parallel_pipeline, fillvalue="-"):
i += 1
logger.info(f"stage{i}: {' | '.join(op_list)}")
else:
raise ValueError(f"unknown pipeline.type={type(pipeline)}")
@timer(name="pipeline.execute")
def __call__(self, enable_print: bool = True):
if enable_print:
self.print_pipeline()
for i, pipeline in enumerate(self.pipeline_list):
with Timer(f"step_{i}"):
if isinstance(pipeline, str):
self._execute_sub_pipeline(pipeline)
else:
future_list = []
for sub_pipeline in pipeline:
future = self.context.thread_pool.submit(self._execute_sub_pipeline, pipeline=sub_pipeline)
future_list.append(future)
for future in as_completed(future_list):
future.result()

View file

@ -1,37 +0,0 @@
from concurrent.futures import ThreadPoolExecutor
from typing import Dict
from experiencemaker.schema.app_config import AppConfig
from experiencemaker.vector_store.base_vector_store import BaseVectorStore
class PipelineContext:
def __init__(self, **kwargs):
self._context: dict = {**kwargs}
def get_context(self, key: str, default=None):
return self._context.get(key, default)
def set_context(self, key: str, value):
self._context[key] = value
@property
def request(self):
return self._context["request"]
@property
def response(self):
return self._context["response"]
@property
def app_config(self) -> AppConfig:
return self._context["app_config"]
@property
def thread_pool(self) -> ThreadPoolExecutor:
return self._context["thread_pool"]
@property
def vector_store_dict(self) -> Dict[str, BaseVectorStore]:
return self._context["vector_store_dict"]

View file

@ -1,69 +0,0 @@
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class HttpServiceConfig:
host: str = field(default="0.0.0.0")
port: int = field(default=8001)
timeout_keep_alive: int = field(default=600)
limit_concurrency: int = field(default=64)
@dataclass
class ThreadPoolConfig:
max_workers: int = field(default=10)
@dataclass
class APIConfig:
retriever: str = field(default="")
summarizer: str = field(default="")
vector_store: str = field(default="")
agent: str = field(default="")
@dataclass
class OpConfig:
backend: str = field(default="")
prompt_file_path: str = field(default="")
prompt_dict: dict = field(default_factory=dict)
llm: str = field(default="")
embedding_model: str = field(default="")
vector_store: str = field(default="")
params: dict = field(default_factory=dict)
@dataclass
class LLMConfig:
backend: str = field(default="")
model_name: str = field(default="")
params: dict = field(default_factory=dict)
@dataclass
class EmbeddingModelConfig:
backend: str = field(default="")
model_name: str = field(default="")
params: dict = field(default_factory=dict)
@dataclass
class VectorStoreConfig:
backend: str = field(default="")
embedding_model: str = field(default="")
params: dict = field(default_factory=dict)
@dataclass
class AppConfig:
pre_defined_config: str = field(default="default_config")
config_path: str = field(default="")
mcp_transport: str = field(default="sse")
http_service: HttpServiceConfig = field(default_factory=HttpServiceConfig)
thread_pool: ThreadPoolConfig = field(default_factory=ThreadPoolConfig)
api: APIConfig = field(default_factory=APIConfig)
op: Dict[str, OpConfig] = field(default_factory=dict)
llm: Dict[str, LLMConfig] = field(default_factory=dict)
embedding_model: Dict[str, EmbeddingModelConfig] = field(default_factory=dict)
vector_store: Dict[str, VectorStoreConfig] = field(default_factory=dict)

View file

@ -1,144 +0,0 @@
import datetime
from abc import ABC
from typing import List
from uuid import uuid4
from loguru import logger
from pydantic import BaseModel, Field
from experiencemaker.schema.vector_node import VectorNode
class ExperienceMeta(BaseModel):
author: str = Field(default="")
created_time: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
modified_time: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
extra_info: dict | None = Field(default=None)
def update_modified_time(self):
self.modified_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
class BaseExperience(BaseModel, ABC):
workspace_id: str = Field(default="")
experience_id: str = Field(default_factory=lambda: uuid4().hex)
experience_type: str = Field(default="")
when_to_use: str = Field(default="")
content: str | bytes = Field(default="")
score: float | None = Field(default=None)
metadata: ExperienceMeta = Field(default_factory=ExperienceMeta)
def to_vector_node(self) -> VectorNode:
raise NotImplementedError
@classmethod
def from_vector_node(cls, node: VectorNode):
raise NotImplementedError
class TextExperience(BaseExperience):
experience_type: str = Field(default="text")
def to_vector_node(self) -> VectorNode:
return VectorNode(unique_id=self.experience_id,
workspace_id=self.workspace_id,
content=self.when_to_use,
metadata={
"experience_type": self.experience_type,
"experience_content": self.content,
"score": self.score,
"metadata": self.metadata.model_dump(),
})
@classmethod
def from_vector_node(cls, node: VectorNode):
return cls(workspace_id=node.workspace_id,
experience_id=node.unique_id,
experience_type=node.metadata.get("experience_type"),
when_to_use=node.content,
content=node.metadata.get("experience_content"),
score=node.metadata.get("score"),
metadata=node.metadata.get("metadata"))
class FunctionArg(BaseModel):
arg_name: str = Field(default=...)
arg_type: str = Field(default=...)
required: bool = Field(default=True)
class Function(BaseModel):
func_code: str = Field(default=..., description="function code")
func_name: str = Field(default=..., description="function name")
func_args: List[FunctionArg] = Field(default_factory=list)
class FuncExperience(BaseExperience):
experience_type: str = Field(default="function")
functions: List[Function] = Field(default_factory=list)
class PersonalExperience(BaseExperience):
experience_type: str = Field(default="personal")
person: str = Field(default="")
topic: str = Field(default="")
class KnowledgeExperience(BaseExperience):
experience_type: str = Field(default="knowledge")
topic: str = Field(default="")
def vector_node_to_experience(node: VectorNode) -> BaseExperience:
experience_type = node.metadata.get("experience_type")
if experience_type == "text":
return TextExperience.from_vector_node(node)
elif experience_type == "function":
return FuncExperience.from_vector_node(node)
elif experience_type == "personal":
return PersonalExperience.from_vector_node(node)
elif experience_type == "knowledge":
return KnowledgeExperience.from_vector_node(node)
else:
logger.warning(f"experience type {experience_type} not supported")
return TextExperience.from_vector_node(node)
def dict_to_experience(experience_dict: dict) -> BaseExperience:
experience_type = experience_dict.get("experience_type", "text")
if experience_type == "text":
return TextExperience(**experience_dict)
elif experience_type == "function":
return FuncExperience(**experience_dict)
elif experience_type == "personal":
return PersonalExperience(**experience_dict)
elif experience_type == "knowledge":
return KnowledgeExperience(**experience_dict)
else:
logger.warning(f"experience type {experience_type} not supported")
return TextExperience(**experience_dict)
if __name__ == "__main__":
e1 = TextExperience(
workspace_id="w_1024",
experience_id="123",
when_to_use="test case use",
content="test content",
score=0.99,
metadata=ExperienceMeta(author="user"))
print(e1.model_dump_json(indent=2))
v1 = e1.to_vector_node()
print(v1.model_dump_json(indent=2))
e2 = vector_node_to_experience(v1)
print(e2.model_dump_json(indent=2))

View file

@ -1,68 +0,0 @@
import json
from typing import List
from pydantic import BaseModel, Field, model_validator
from experiencemaker.enumeration.role import Role
class ToolCall(BaseModel):
index: int = Field(default=0)
id: str = Field(default="")
name: str = Field(default="")
arguments: str = Field(default="")
type: str = Field(default="function")
@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) -> dict:
return json.loads(self.arguments)
def simple_dump(self) -> dict:
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)
tool_call_id: str = Field(default="")
metadata: dict = Field(default_factory=dict)
def simple_dump(self, add_reason_when_empty: bool = True) -> dict:
result: dict
if self.content:
result = {"role": self.role.value, "content": self.content}
elif add_reason_when_empty and self.reasoning_content:
result = {"role": self.role.value, "content": self.reasoning_content}
else:
result = {"role": self.role.value, "content": ""}
if self.tool_calls:
result["tool_calls"] = [x.simple_dump() for x in self.tool_calls]
return result
class Trajectory(BaseModel):
task_id: str = Field(default="")
messages: List[Message] = Field(default_factory=list)
score: float = Field(default=0.0)
metadata: dict = Field(default_factory=dict)

View file

@ -1,35 +0,0 @@
from typing import List
from pydantic import BaseModel, Field
from experiencemaker.schema.message import Message, Trajectory
class BaseRequest(BaseModel):
workspace_id: str = Field(default="default")
config: dict = Field(default_factory=dict)
class RetrieverRequest(BaseRequest):
query: str = Field(default="")
messages: List[Message] = Field(default_factory=list)
top_k: int = Field(default=1)
class SummarizerRequest(BaseRequest):
traj_list: List[Trajectory] = Field(default_factory=list)
class VectorStoreRequest(BaseRequest):
action: str = Field(default="")
src_workspace_id: str = Field(default="")
path: str = Field(default="")
experience_ids: List[str] = Field(default_factory=list)
freq_threshold: int = Field(default=5)
utility_threshold: float = Field(default=0.5)
class AgentRequest(BaseRequest):
query: str = Field(default="")
messages: List[Message] = Field(default_factory=list)

View file

@ -1,29 +0,0 @@
from typing import List
from pydantic import BaseModel, Field
from experiencemaker.schema.experience import BaseExperience
from experiencemaker.schema.message import Message
class BaseResponse(BaseModel):
success: bool = Field(default=True)
metadata: dict = Field(default_factory=dict)
class RetrieverResponse(BaseResponse):
experience_list: List[BaseExperience] = Field(default_factory=list)
experience_merged: str = Field(default="")
class SummarizerResponse(BaseResponse):
experience_list: List[BaseExperience] = Field(default_factory=list)
deleted_experience_ids: List[str] = Field(default_factory=list)
class VectorStoreResponse(BaseResponse):
...
class AgentResponse(BaseResponse):
answer: str = Field(default="")
messages: List[Message] = Field(default_factory=list)

View file

@ -1,13 +0,0 @@
from typing import List
from uuid import uuid4
from pydantic import BaseModel, Field
class VectorNode(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)
freq: int = Field(default=0)
utility: int = Field(default=0)
metadata: dict = Field(default_factory=dict)

View file

@ -1,37 +0,0 @@
from pydantic import Field
from experiencemaker.schema.message import Trajectory, Message
from experiencemaker.schema.request import RetrieverRequest, SummarizerRequest, VectorStoreRequest, AgentRequest
from experiencemaker.schema.response import RetrieverResponse, SummarizerResponse, VectorStoreResponse, AgentResponse
from experiencemaker.utils.http_client import HttpClient
class ExperienceMakerClient(HttpClient):
base_url: str = Field(default="http://0.0.0.0:8001")
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()
workspace_id = "t123"
response = client.call_summarizer(
SummarizerRequest(workspace_id=workspace_id,
traj_list=[Trajectory(messages=[Message(content="hello world!")])]))
print(response.model_dump())
response = client.call_retriever(RetrieverRequest(workspace_id=workspace_id, query="hello world"))
print(response.model_dump())

View file

@ -1,96 +0,0 @@
from concurrent.futures import ThreadPoolExecutor
from typing import List
from loguru import logger
from experiencemaker.config.config_parser import ConfigParser
from experiencemaker.embedding_model import EMBEDDING_MODEL_REGISTRY
from experiencemaker.pipeline.pipeline import Pipeline
from experiencemaker.pipeline.pipeline_context import PipelineContext
from experiencemaker.schema.app_config import AppConfig, HttpServiceConfig, EmbeddingModelConfig
from experiencemaker.schema.request import SummarizerRequest, RetrieverRequest, VectorStoreRequest, AgentRequest, \
BaseRequest
from experiencemaker.schema.response import SummarizerResponse, RetrieverResponse, VectorStoreResponse, AgentResponse, \
BaseResponse
from experiencemaker.vector_store import VECTOR_STORE_REGISTRY
class ExperienceMakerService:
def __init__(self, args: List[str]):
self.config_parser = ConfigParser(args)
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 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"
vector_store_cls = VECTOR_STORE_REGISTRY[config.backend]
assert config.embedding_model in self.init_app_config.embedding_model, \
f"embedding_model={config.embedding_model} is not existed"
embedding_model_config: EmbeddingModelConfig = self.init_app_config.embedding_model[config.embedding_model]
assert embedding_model_config.backend in EMBEDDING_MODEL_REGISTRY, \
f"embedding_model={embedding_model_config.backend} is not existed"
embedding_model_cls = EMBEDDING_MODEL_REGISTRY[embedding_model_config.backend]
embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
**embedding_model_config.params)
self.vector_store_dict[name] = vector_store_cls(embedding_model=embedding_model, **config.params)
@property
def http_service_config(self) -> HttpServiceConfig:
return self.init_app_config.http_service
def __call__(self, api: str, request: dict | BaseRequest) -> BaseResponse:
if isinstance(request, dict):
app_config: AppConfig = self.config_parser.get_app_config(**request["config"])
else:
app_config: AppConfig = self.config_parser.get_app_config(**request.config)
if api == "retriever":
if isinstance(request, dict):
request = RetrieverRequest(**request)
response = RetrieverResponse()
pipeline = app_config.api.retriever
elif api == "summarizer":
if isinstance(request, dict):
request = SummarizerRequest(**request)
response = SummarizerResponse()
pipeline = app_config.api.summarizer
elif api == "vector_store":
if isinstance(request, dict):
request = VectorStoreRequest(**request)
response = VectorStoreResponse()
pipeline = app_config.api.vector_store
elif api == "agent":
if isinstance(request, dict):
request = AgentRequest(**request)
response = AgentResponse()
pipeline = app_config.api.agent
else:
raise RuntimeError(f"Invalid service.api={api}")
logger.info(f"request={request.model_dump_json()}")
try:
context = PipelineContext(app_config=app_config,
thread_pool=self.thread_pool,
request=request,
response=response,
vector_store_dict=self.vector_store_dict)
pipeline = Pipeline(pipeline=pipeline, context=context)
pipeline()
except Exception as e:
logger.exception(f"api={api} encounter error={e.args}")
response.success = False
response.metadata["error"] = str(e)
return response

View file

@ -1,87 +0,0 @@
import asyncio
import json
from typing import List
from fastmcp import Client
from pydantic import BaseModel, Field
from experiencemaker.schema.request import RetrieverRequest, SummarizerRequest, VectorStoreRequest, AgentRequest
from experiencemaker.schema.response import RetrieverResponse, SummarizerResponse, VectorStoreResponse, AgentResponse
class MCPClient(BaseModel):
base_url: str = Field(default="http://0.0.0.0:8001/sse")
enable_sse: bool = Field(default=True)
timeout: int = Field(default=300)
_client: Client | None = None
async def __aenter__(self):
if self.enable_sse:
self._client = Client(self.base_url)
else:
self._client = Client("stdio")
await self._client.__aenter__()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._client:
await self._client.__aexit__(exc_type, exc_val, exc_tb)
async def list_tools(self) -> List[str]:
tools = await self._client.list_tools()
return [tool.name for tool in tools]
async def call_retriever(self, request: RetrieverRequest) -> RetrieverResponse:
result = await self._client.call_tool("retriever", request.model_dump())
return RetrieverResponse(**result.structured_content)
async def call_summarizer(self, request: SummarizerRequest) -> SummarizerResponse:
result = await self._client.call_tool("summarizer", request.model_dump())
return SummarizerResponse(**result.structured_content)
async def call_vector_store(self, request: VectorStoreRequest) -> VectorStoreResponse:
result = await self._client.call_tool("vector_store", request.model_dump())
return VectorStoreResponse(**result.structured_content)
async def call_agent(self, request: AgentRequest) -> AgentResponse:
result = await self._client.call_tool("agent", request.model_dump())
return AgentResponse(**result.structured_content)
async def main():
"""Example usage of MCPClient"""
async with MCPClient() as client:
# List available tools
tools = await client.list_tools()
print("Available tools:", json.dumps(tools, ensure_ascii=False, indent=2))
# Example retriever call
retriever_request = RetrieverRequest(
workspace_id="test_workspace",
query="hello world",
top_k=5)
try:
response = await client.call_retriever(retriever_request)
print("Retriever response:", response.model_dump())
except Exception as e:
print(f"Error calling retriever: {e}")
# Example summarizer call
from experiencemaker.schema.message import Trajectory, Message
summarizer_request = SummarizerRequest(
workspace_id="test_workspace",
traj_list=[Trajectory(messages=[Message(content="hello world!")])])
try:
response = await client.call_summarizer(summarizer_request)
print("Summarizer response:", response.model_dump())
except Exception as e:
print(f"Error calling summarizer: {e}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,9 +0,0 @@
from experiencemaker.utils.registry import Registry
TOOL_REGISTRY = Registry()
from experiencemaker.tool.code_tool import CodeTool
from experiencemaker.tool.dashscope_search_tool import DashscopeSearchTool
from experiencemaker.tool.tavily_search_tool import TavilySearchTool
from experiencemaker.tool.terminate_tool import TerminateTool
from experiencemaker.tool.mcp_tool import MCPTool

View file

@ -1,80 +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.cached_result.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
def simple_dump(self) -> dict:
"""
It may be in other different tool params formats; different versions are completed here.
"""
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,43 +0,0 @@
import sys
from io import StringIO
from experiencemaker.tool import TOOL_REGISTRY
from experiencemaker.tool.base_tool import BaseTool
@TOOL_REGISTRY.register()
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

@ -1,162 +0,0 @@
import os
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 import TOOL_REGISTRY
from experiencemaker.tool.base_tool import BaseTool
@TOOL_REGISTRY.register()
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():
load_dotenv()
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

@ -1,77 +0,0 @@
import asyncio
from typing import List
from mcp import ClientSession
from mcp.client.sse import sse_client
from pydantic import Field, model_validator
from experiencemaker.tool import TOOL_REGISTRY
from experiencemaker.tool.base_tool import BaseTool
@TOOL_REGISTRY.register()
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")
@model_validator(mode="after")
def refresh_tools(self):
self.refresh()
return self
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 refresh(self):
self.tool_name_list.clear()
self.cache_tools.clear()
if "sse" in self.server_url:
original_tool_list = asyncio.run(self._get_tools())
for tool in original_tool_list.tools:
self.cache_tools[tool.name] = tool
self.tool_name_list.append(tool.name)
else:
raise NotImplementedError("Non-SSE refresh not implemented yet")
@property
def input_schema(self) -> dict:
return {x: self.cache_tools[x].inputSchema for x in self.cache_tools}
@property
def output_schema(self) -> dict:
raise NotImplementedError("Output schema not implemented yet")
def get_tool_description(self, tool_name: str, schema: bool = False) -> str:
if tool_name not in self.cache_tools:
raise RuntimeError(f"Tool {tool_name} not found")
tool = self.cache_tools.get(tool_name)
description = f"tool={tool_name} description={tool.description}\n"
if schema:
description += f"input_schema={self.input_schema[tool_name]}\n" \
f"output_schema={self.output_schema[tool_name]}\n"
return description.strip()
async def async_execute(self, tool_name: str, **kwargs):
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, kwargs)
return results.content[0].text, results.isError
else:
raise NotImplementedError("Non-SSE execute not implemented yet")
def _execute(self, **kwargs):
return asyncio.run(self.async_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

@ -1,109 +0,0 @@
import json
import os
import re
import time
from typing import Literal
from loguru import logger
from pydantic import Field, model_validator, PrivateAttr
from tavily import TavilyClient
from experiencemaker.tool import TOOL_REGISTRY
from experiencemaker.tool.base_tool import BaseTool
@TOOL_REGISTRY.register()
class TavilySearchTool(BaseTool):
name: str = "web_search"
description: str = "Use query to retrieve relevant information from the internet."
parameters: dict = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "search query",
}
},
"required": ["query"]
}
enable_print: bool = Field(default=True)
enable_cache: bool = Field(default=False)
cache_path: str = Field(default="./web_search_cache")
topic: Literal["general", "news", "finance"] = Field(default="general", description="finance, general")
_client: TavilyClient | None = PrivateAttr()
@model_validator(mode="after")
def init(self):
if not os.path.exists(self.cache_path):
os.makedirs(self.cache_path)
self._client = TavilyClient()
return self
def load_cache(self, cache_name: str = "default") -> dict:
cache_file = os.path.join(self.cache_path, cache_name + ".jsonl")
if not os.path.exists(cache_file):
return {}
with open(cache_file) as f:
return json.load(f)
def dump_cache(self, cache_dict: dict, cache_name: str = "default"):
cache_file = os.path.join(self.cache_path, cache_name + ".jsonl")
with open(cache_file, "w") as f:
return json.dump(cache_dict, f, indent=2, ensure_ascii=False)
@staticmethod
def remove_urls_and_images(text):
pattern = re.compile(r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]')
result = pattern.sub("", text)
return result
def post_process(self, response):
if self.enable_print:
logger.info("response=\n" + json.dumps(response, indent=2, ensure_ascii=False))
return response
def execute(self, query: str = "", **kwargs):
assert query, "Query cannot be empty"
cache_dict = {}
if self.enable_cache:
cache_dict = self.load_cache()
if query in cache_dict:
return self.post_process(cache_dict[query])
for i in range(self.max_retries):
try:
response = self._client.search(query=query, topic=self.topic)
url_info_dict = {item["url"]: item for item in response["results"]}
response_extract = self._client.extract(urls=[item["url"] for item in response["results"]],
format="text")
final_result = {}
for item in response_extract["results"]:
url = item["url"]
final_result[url] = url_info_dict[url]
final_result[url]["raw_content"] = item["raw_content"]
if self.enable_cache:
cache_dict[query] = final_result
self.dump_cache(cache_dict)
return self.post_process(final_result)
except Exception as e:
logger.exception(f"tavily search with query={query} encounter error with e={e.args}")
time.sleep(i + 1)
return None
if __name__ == "__main__":
from dotenv import load_dotenv
load_dotenv()
tool = TavilySearchTool()
tool.execute(query="A股医药为什么一直涨")

View file

@ -1,23 +0,0 @@
from experiencemaker.tool import TOOL_REGISTRY
from experiencemaker.tool.base_tool import BaseTool
@TOOL_REGISTRY.register()
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

@ -1,17 +0,0 @@
import re
def camel_to_snake(content: str) -> str:
"""
BaseWorker -> base_worker
"""
snake_str = re.sub(r'(?<!^)(?=[A-Z])', '_', content).lower()
return snake_str
def snake_to_camel(content: str) -> str:
"""
base_worker -> BaseWorker
"""
camel_str = "".join(x.capitalize() for x in content.split("_"))
return camel_str

View file

@ -1,25 +0,0 @@
import json
from pathlib import Path
import yaml
class FileHandler:
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
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

@ -1,156 +0,0 @@
import http
import time
from typing import Any
import requests
from loguru import logger
from pydantic import BaseModel, Field, PrivateAttr, model_validator
from experiencemaker.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(default=0.5, description="interval time for retry")
retry_time_multiplier: float = Field(default=2.0, description="retry time multiplier")
retry_max_count: int = Field(default=1, description="maximum number of retries")
_client: Any = PrivateAttr()
@model_validator(mode="after")
def init_client(self):
self._client = requests.Session() if self.keep_alive else requests
return self
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)
return None
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 None
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)
return None

View file

@ -1,102 +0,0 @@
from typing import List
from experiencemaker.enumeration.role import Role
from experiencemaker.schema.message import Message, Trajectory
import json
import re
from loguru import logger
def merge_messages_content(messages: List[Message | dict]) -> str:
content_collector = []
for i, message in enumerate(messages):
if isinstance(message, dict):
message = Message(**message)
if message.role is Role.ASSISTANT:
line = f"### step.{i} role={message.role.value} content=\n{message.reasoning_content}\n\n{message.content}\n"
if message.tool_calls:
for tool_call in message.tool_calls:
line += f" - tool call={tool_call.name}\n params={tool_call.arguments}\n"
content_collector.append(line)
elif message.role is Role.USER:
line = f"### step.{i} role={message.role.value} content=\n{message.content}\n"
content_collector.append(line)
elif message.role is Role.TOOL:
line = f"### step.{i} role={message.role.value} tool call result=\n{message.content}\n"
content_collector.append(line)
return "\n".join(content_collector)
def parse_json_experience_response(response: str) -> List[dict]:
"""Parse JSON formatted experience response"""
try:
# Extract JSON blocks
json_pattern = r'```json\s*([\s\S]*?)\s*```'
json_blocks = re.findall(json_pattern, response)
if json_blocks:
parsed = json.loads(json_blocks[0])
# Handle array format
if isinstance(parsed, list):
experiences = []
for exp_data in parsed:
if isinstance(exp_data, dict) and (
("when_to_use" in exp_data and "experience" in exp_data) or
("condition" in exp_data and "experience" in exp_data)
):
experiences.append(exp_data)
return experiences
# Handle single object
elif isinstance(parsed, dict) and (
("when_to_use" in parsed and "experience" in parsed) or
("condition" in parsed and "experience" in parsed)
):
return [parsed]
# Fallback: try to parse entire response
parsed = json.loads(response)
if isinstance(parsed, list):
return parsed
elif isinstance(parsed, dict):
return [parsed]
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse JSON experience response: {e}")
return []
def get_trajectory_context(trajectory: Trajectory, step_sequence: List[Message]) -> str:
"""Get context of step sequence within trajectory"""
try:
# Find position of step sequence in trajectory
start_idx = 0
for i, step in enumerate(trajectory.messages):
if step == step_sequence[0]:
start_idx = i
break
# Extract before and after context
context_before = trajectory.messages[max(0, start_idx - 2):start_idx]
context_after = trajectory.messages[start_idx + len(step_sequence):start_idx + len(step_sequence) + 2]
context = f"Query: {trajectory.metadata.get('query', 'N/A')}\n"
if context_before:
context += "Previous steps:\n" + "\n".join(
[f"- {step.content[:100]}..." for step in context_before]) + "\n"
if context_after:
context += "Following steps:\n" + "\n".join([f"- {step.content[:100]}..." for step in context_after])
return context
except Exception as e:
logger.error(f"Error getting trajectory context: {e}")
return f"Query: {trajectory.metadata.get('query', 'N/A')}"

View file

@ -1,33 +0,0 @@
from typing import List
from loguru import logger
from experiencemaker.utils.common_utils import camel_to_snake
class Registry(object):
def __init__(self):
self._registry = {}
def register(self, name: str = ""):
def decorator(cls):
class_name = name if name else camel_to_snake(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_names(self) -> List[str]:
return sorted(self._registry.keys())

View file

@ -1,9 +0,0 @@
def singleton(cls):
_instance = {}
def _singleton(*args, **kwargs):
if cls not in _instance:
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]
return _singleton

View file

@ -1,53 +0,0 @@
import time
from loguru import logger
class Timer(object):
def __init__(self, name: str, use_ms: bool = False, stack_level: int = 2):
self.name: str = name
self.use_ms: bool = use_ms
self.stack_level: int = stack_level
self.time_start: float = 0
self.time_end: float = 0
self.time_cost: float = 0
def __enter__(self, *args, **kwargs):
self.time_start = time.time()
logger.info(f"---------- enter {self.name} ----------", stacklevel=self.stack_level)
return self
def __exit__(self, *args):
self.time_end = time.time()
self.time_cost = self.time_end - self.time_start
if self.use_ms:
time_str = f"{self.time_cost * 1000:.2f}ms"
else:
time_str = f"{self.time_cost:.3f}s"
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):
def decorator(func):
def wrapper(*args, **kwargs):
with Timer(name=name or func.__name__, use_ms=use_ms, stack_level=stack_level + 1):
return func(*args, **kwargs)
return wrapper
return decorator
if __name__ == "__main__":
import random
@timer("run_func_final", use_ms=True)
def run_func():
time.sleep(random.uniform(0.05, 0.15))
print("done")
run_func()

Some files were not shown because too many files have changed in this diff Show more