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
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 417 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1,009 KiB After Width: | Height: | Size: 1,009 KiB |
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 508 KiB |
|
|
@ -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
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# from .app import main
|
||||
|
||||
__version__ = "0.1.1"
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
|
||||
# python -m build
|
||||
# twine upload dist/*
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from experiencemaker.utils.registry import Registry
|
||||
|
||||
EMBEDDING_MODEL_REGISTRY = Registry()
|
||||
|
||||
from experiencemaker.embedding_model.openai_compatible_embedding_model import OpenAICompatibleEmbeddingModel
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class AgentState(str, Enum):
|
||||
IDLE = "idle"
|
||||
RUNNING = "running"
|
||||
COMPLETE = "complete"
|
||||
FAILED = "failed"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class ChunkEnum(str, Enum):
|
||||
THINK = "think"
|
||||
ANSWER = "answer"
|
||||
TOOL = "tool"
|
||||
USAGE = "usage"
|
||||
ERROR = "error"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class HttpEnum(str, Enum):
|
||||
GET = "get"
|
||||
POST = "post"
|
||||
HEAD = "head"
|
||||
PUT = "put"
|
||||
DELETE = "delete"
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Role(str, Enum):
|
||||
SYSTEM = "system"
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
TOOL = "tool"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from experiencemaker.utils.registry import Registry
|
||||
|
||||
LLM_REGISTRY = Registry()
|
||||
|
||||
from experiencemaker.llm.openai_compatible_llm import OpenAICompatibleBaseLLM
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
...
|
||||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
"""
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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!'"))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()))}"
|
||||
|
|
@ -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股医药为什么一直涨")
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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')}"
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||