From a17dc024fa80dbce913a2b2f3129db12e447a913 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Tue, 10 Jun 2025 10:51:37 +0800 Subject: [PATCH] add http sevice --- .../module/agent_wrapper/simple_agent.py | 3 +- .../base_context_generator.py | 1 - .../module/evaluator/base_evaluator.py | 4 +- experiencemaker/module/runner/base_runner.py | 4 +- .../module/summarizer/base_summarizer.py | 2 - experiencemaker/schema/request.py | 7 +- experiencemaker/schema/response.py | 5 +- experiencemaker/service.py | 118 ------------- .../service/experience_maker_service.py | 164 ++++++++++++++++++ experiencemaker/service/http_service.py | 50 ++++++ 10 files changed, 226 insertions(+), 132 deletions(-) delete mode 100644 experiencemaker/service.py create mode 100644 experiencemaker/service/experience_maker_service.py create mode 100644 experiencemaker/service/http_service.py diff --git a/experiencemaker/module/agent_wrapper/simple_agent.py b/experiencemaker/module/agent_wrapper/simple_agent.py index 2e0ced3c..b2db9b24 100644 --- a/experiencemaker/module/agent_wrapper/simple_agent.py +++ b/experiencemaker/module/agent_wrapper/simple_agent.py @@ -8,6 +8,7 @@ from pydantic import Field, BaseModel from experiencemaker.model.base_llm import BaseLLM from experiencemaker.module.prompt.prompt_mixin import PromptMixin from experiencemaker.schema.trajectory import Message, ActionMessage, ToolCall, StateMessage +from experiencemaker.tool import CodeTool, DashscopeSearchTool, TerminateTool from experiencemaker.tool.base_tool import BaseTool @@ -23,7 +24,7 @@ class SimpleAgentContext(BaseModel): class SimpleAgent(PromptMixin): llm: BaseLLM | None = Field(default=None) max_steps: int = Field(default=10) - tools: List[BaseTool] = Field(default_factory=list) + tools: List[BaseTool] = [CodeTool(), DashscopeSearchTool(), TerminateTool()] prompt_file_path: Path = Path(__file__).parent / "simple_agent_prompt.yaml" def think(self, context: SimpleAgentContext): diff --git a/experiencemaker/module/context_generator/base_context_generator.py b/experiencemaker/module/context_generator/base_context_generator.py index ebe61af7..e7839a12 100644 --- a/experiencemaker/module/context_generator/base_context_generator.py +++ b/experiencemaker/module/context_generator/base_context_generator.py @@ -14,7 +14,6 @@ from experiencemaker.utils.registry import Registry class BaseContextGenerator(BaseModel, ABC): vector_store: BaseVectorStore | None = Field(default=None) llm: BaseLLM | None = Field(default=None) - embedding_model: BaseEmbeddingModel | None = Field(default=None) workspace_id: str = Field(default="") def _build_retrieve_query(self, trajectory: Trajectory, **kwargs) -> str: diff --git a/experiencemaker/module/evaluator/base_evaluator.py b/experiencemaker/module/evaluator/base_evaluator.py index 13e67575..e2de7d44 100644 --- a/experiencemaker/module/evaluator/base_evaluator.py +++ b/experiencemaker/module/evaluator/base_evaluator.py @@ -2,7 +2,7 @@ from abc import ABC from pydantic import BaseModel, Field -from experiencemaker.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper +from experiencemaker.module.agent_wrapper.agent_wrapper_mixin import AgentWrapperMixin from experiencemaker.module.context_generator.base_context_generator import BaseContextGenerator from experiencemaker.module.environment.base_environment import BaseEnvironment from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer @@ -10,7 +10,7 @@ from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer class BaseEvaluator(BaseModel, ABC): data_path: str = Field(default="") - agent_wrapper: BaseAgentWrapper | None = Field(default=None) + agent_wrapper: AgentWrapperMixin | None = Field(default=None) context_generator: BaseContextGenerator | None = Field(default=None) summarizer: BaseSummarizer | None = Field(default=None) env: BaseEnvironment | None = Field(default=None) diff --git a/experiencemaker/module/runner/base_runner.py b/experiencemaker/module/runner/base_runner.py index f5f80a76..e0db5abc 100644 --- a/experiencemaker/module/runner/base_runner.py +++ b/experiencemaker/module/runner/base_runner.py @@ -2,7 +2,7 @@ from typing import List from pydantic import BaseModel, Field -from experiencemaker.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper +from experiencemaker.module.agent_wrapper.agent_wrapper_mixin import AgentWrapperMixin from experiencemaker.module.context_generator.base_context_generator import BaseContextGenerator from experiencemaker.module.environment.base_environment import BaseEnvironment from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer @@ -10,7 +10,7 @@ from experiencemaker.schema.trajectory import Trajectory class BaseRunner(BaseModel): - agent_wrapper: BaseAgentWrapper | None = Field(default=None) + agent_wrapper: AgentWrapperMixin | None = Field(default=None) context_generator: BaseContextGenerator | None = Field(default=None) summarizer: BaseSummarizer | None = Field(default=None) env: BaseEnvironment | None = Field(default=None) diff --git a/experiencemaker/module/summarizer/base_summarizer.py b/experiencemaker/module/summarizer/base_summarizer.py index d5f93c40..53ae4a4a 100644 --- a/experiencemaker/module/summarizer/base_summarizer.py +++ b/experiencemaker/module/summarizer/base_summarizer.py @@ -3,7 +3,6 @@ from typing import List from pydantic import Field, BaseModel -from experiencemaker.model.base_embedding_model import BaseEmbeddingModel from experiencemaker.model.base_llm import BaseLLM from experiencemaker.schema.experience import Experience from experiencemaker.schema.trajectory import Trajectory @@ -15,7 +14,6 @@ from experiencemaker.utils.registry import Registry class BaseSummarizer(BaseModel, ABC): vector_store: BaseVectorStore | None = Field(default=None) llm: BaseLLM | None = Field(default=None) - embedding_model: BaseEmbeddingModel | None = Field(default=None) workspace_id: str = Field(default="") def _extract_experiences(self, trajectories: List[Trajectory], **kwargs) -> List[Experience]: diff --git a/experiencemaker/schema/request.py b/experiencemaker/schema/request.py index 71f6445e..57e23fcc 100644 --- a/experiencemaker/schema/request.py +++ b/experiencemaker/schema/request.py @@ -1,13 +1,12 @@ from abc import ABC from typing import List -from pydantic import Field +from pydantic import BaseModel, Field -from experiencemaker.schema.module_loader import ModuleLoader from experiencemaker.schema.trajectory import Trajectory -class BaseRequest(ModuleLoader, ABC): +class BaseRequest(BaseModel, ABC): metadata: dict = Field(default_factory=dict) @@ -21,4 +20,4 @@ class ContextGeneratorRequest(BaseRequest): class SummarizerRequest(BaseRequest): trajectories: List[Trajectory] = Field(default_factory=dict) - return_samples: bool = Field(default=False) + return_experience: bool = Field(default=True) diff --git a/experiencemaker/schema/response.py b/experiencemaker/schema/response.py index 8a0e3caa..feed1b44 100644 --- a/experiencemaker/schema/response.py +++ b/experiencemaker/schema/response.py @@ -3,7 +3,8 @@ from typing import List from pydantic import BaseModel, Field -from experiencemaker.schema.trajectory import Trajectory, ContextMessage, Sample +from experiencemaker.schema.experience import Experience +from experiencemaker.schema.trajectory import Trajectory, ContextMessage class BaseResponse(BaseModel, ABC): @@ -20,4 +21,4 @@ class ContextGeneratorResponse(BaseResponse): class SummarizerResponse(BaseResponse): - extract_samples: List[Sample] = Field(default_factory=list) + experiences: List[Experience] = Field(default_factory=list) diff --git a/experiencemaker/service.py b/experiencemaker/service.py deleted file mode 100644 index 24fe1113..00000000 --- a/experiencemaker/service.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import List - -import uvicorn -from fastapi import FastAPI -from loguru import logger - -from experiencemaker.model.base_embedding_model import BaseEmbeddingModel, EMBEDDING_MODEL_REGISTRY -from experiencemaker.model.base_llm import BaseLLM, LLM_REGISTRY -from experiencemaker.module.agent_wrapper.base_agent_wrapper import BaseAgentWrapper -from experiencemaker.module.context_generator.base_context_generator import BaseContextGenerator -from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer -from experiencemaker.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest -from experiencemaker.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse -from experiencemaker.schema.trajectory import ContextMessage, Trajectory, Sample -from experiencemaker.storage.base_vector_store import BaseVectorStore, VECTOR_STORE_REGISTRY - -app = FastAPI() -from pydantic import BaseModel, Field, model_validator - - -class ExperienceMakerHttpService(BaseModel): - host: str = Field(default="0.0.0.0") - port: int = Field(default=8001) - timeout_keep_alive: int = Field(default=600000) - limit_concurrency: int = Field(default=32) - - llm_config: dict = Field(default_factory=dict) - embedding_model_config: dict = Field(default_factory=dict) - vector_store_config: dict = Field(default_factory=dict) - agent_wrapper_config: dict = Field(default_factory=dict) - context_generator_config: dict = Field(default_factory=dict) - summarizer_config: dict = Field(default_factory=dict) - - llm: BaseLLM | None = Field(default=None) - embedding_model: BaseEmbeddingModel | None = Field(default=None) - vector_store: BaseVectorStore | None = Field(default=None) - agent_wrapper: BaseAgentWrapper | None = Field(default=None) - context_generator: BaseContextGenerator | None = Field(default=None) - summarizer: BaseSummarizer | None = Field(default=None) - - @staticmethod - def init_llm(llm_config: dict): - backend = llm_config.pop("backend", None) - assert backend is not None, "llm must have a backend like `openai_compatible`." - assert backend in LLM_REGISTRY, f"llm backend={backend} not supported. supported backend={LLM_REGISTRY.registered_modules}" - llm = LLM_REGISTRY[backend](**llm_config) - logger.info(f"llm is inited with backend={backend} params={llm_config}") - return llm - - @staticmethod - def init_embedding_model(embedding_model_config: dict): - backend = embedding_model_config.pop("backend", None) - assert backend is not None, "embedding_model must have a backend like `openai_compatible`." - assert backend in EMBEDDING_MODEL_REGISTRY, f"embedding_model backend={backend} not supported. supported backend={EMBEDDING_MODEL_REGISTRY.registered_modules}" - embedding_model = EMBEDDING_MODEL_REGISTRY[backend](**embedding_model_config) - logger.info(f"embedding_model is inited with backend={backend} params={embedding_model_config}") - return embedding_model - - @staticmethod - def init_vector_store(vector_store_config: dict): - backend = vector_store_config.pop("backend", None) - assert backend is not None, "vector_store must have a backend like `elasticsearch`." - assert backend in VECTOR_STORE_REGISTRY, f"vector_store backend={backend} not supported. supported backend={VECTOR_STORE_REGISTRY.registered_modules}" - vector_store = VECTOR_STORE_REGISTRY[backend](**vector_store_config) - logger.info(f"vector_store is inited with backend={backend} params={vector_store_config}") - return vector_store - - @model_validator(mode="after") - def init_modules(self): - if self.llm_config: - self.llm = self.init_llm(self.llm_config) - - if self.embedding_model_config: - self.embedding_model = self.init_embedding_model(self.embedding_model_config) - - if self.vector_store_config: - self.vector_store = self.init_vector_store(self.vector_store_config) - - - - - -@app.post('/agent_wrapper', response_model=AgentWrapperResponse) -def call_agent_wrapper(request: AgentWrapperRequest): - module: BaseAgentWrapper = request.load_from_path() - trajectory: Trajectory = module.execute(request.query, **request.metadata) - return AgentWrapperResponse(trajectory=trajectory) - - -@app.post('/context_generator', response_model=ContextGeneratorResponse) -def call_context_generator(request: ContextGeneratorRequest): - module: BaseContextGenerator = request.load_from_path() - context_msg: ContextMessage = module.execute(request.trajectory, **request.metadata) - return ContextGeneratorResponse(context_msg=context_msg) - - -@app.post('/summarizer', response_model=SummarizerResponse) -def call_summarizer(request: SummarizerRequest): - module: BaseSummarizer = request.load_from_path() - samples: List[Sample] = module.execute(request.trajectories, request.return_samples, **request.metadata) - return SummarizerResponse(extract_samples=samples) - - -if __name__ == '__main__': - uvicorn.run(app, host="0.0.0.0", port=8000, timeout_keep_alive=600000, limit_concurrency=32) - # from experiencemaker.config import summarizer_config - # print(summarizer_config.simple) - - from experiencemaker.config.config_handler import ConfigHandler - - summarizer_config = ConfigHandler(module_name="summarizer") - context_generator_config = ConfigHandler(module_name="context_generator") - print(context_generator_config.config_dict) - - - -# launch with: -# python -m experiencemaker.service.model_service diff --git a/experiencemaker/service/experience_maker_service.py b/experiencemaker/service/experience_maker_service.py new file mode 100644 index 00000000..e2a6b98b --- /dev/null +++ b/experiencemaker/service/experience_maker_service.py @@ -0,0 +1,164 @@ +from typing import List + +from loguru import logger +from pydantic import BaseModel, Field, model_validator + +from experiencemaker.model.base_embedding_model import BaseEmbeddingModel, EMBEDDING_MODEL_REGISTRY +from experiencemaker.model.base_llm import BaseLLM, LLM_REGISTRY +from experiencemaker.module.agent_wrapper.agent_wrapper_mixin import AGENT_WRAPPER_REGISTRY, AgentWrapperMixin +from experiencemaker.module.context_generator.base_context_generator import BaseContextGenerator, \ + CONTEXT_GENERATOR_REGISTRY +from experiencemaker.module.summarizer.base_summarizer import BaseSummarizer, SUMMARIZER_REGISTRY +from experiencemaker.schema.experience import Experience +from experiencemaker.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest +from experiencemaker.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse +from experiencemaker.schema.trajectory import Trajectory, ContextMessage +from experiencemaker.storage.base_vector_store import BaseVectorStore, VECTOR_STORE_REGISTRY + + +class ExperienceMakerService(BaseModel): + host: str = Field(default="0.0.0.0") + port: int = Field(default=8001) + timeout_keep_alive: int = Field(default=600000) + limit_concurrency: int = Field(default=32) + + llm_config: dict = Field(default_factory=dict) + embedding_model_config: dict = Field(default_factory=dict) + vector_store_config: dict = Field(default_factory=dict) + agent_wrapper_config: dict = Field(default_factory=dict) + context_generator_config: dict = Field(default_factory=dict) + summarizer_config: dict = Field(default_factory=dict) + + llm: BaseLLM | None = Field(default=None) + embedding_model: BaseEmbeddingModel | None = Field(default=None) + vector_store: BaseVectorStore | None = Field(default=None) + agent_wrapper: AgentWrapperMixin | None = Field(default=None) + context_generator: BaseContextGenerator | None = Field(default=None) + summarizer: BaseSummarizer | None = Field(default=None) + + @staticmethod + def init_llm(llm_config: dict) -> BaseLLM: + backend = llm_config.pop("backend", None) + assert backend is not None, "llm must have a backend like `openai_compatible`." + assert backend in LLM_REGISTRY, f"llm backend={backend} not supported. " \ + f"supported={LLM_REGISTRY.registered_modules}" + llm = LLM_REGISTRY[backend](**llm_config) + logger.info(f"llm is inited with backend={backend} params={llm_config}") + return llm + + def get_llm(self, config: dict, llm: BaseLLM = None) -> BaseLLM: + if "llm" in config: + llm_config = config.pop("llm") + llm = self.init_llm(llm_config) + elif llm is None: + raise RuntimeError("llm must be provided.") + return llm + + @staticmethod + def init_embedding_model(embedding_model_config: dict) -> BaseEmbeddingModel: + backend = embedding_model_config.pop("backend", None) + assert backend is not None, "embedding_model must have a backend like `openai_compatible`." + assert backend in EMBEDDING_MODEL_REGISTRY, f"embedding_model backend={backend} not supported. " \ + f"supported={EMBEDDING_MODEL_REGISTRY.registered_modules}" + embedding_model = EMBEDDING_MODEL_REGISTRY[backend](**embedding_model_config) + logger.info(f"embedding_model is inited with backend={backend} params={embedding_model_config}") + return embedding_model + + def get_embedding_model(self, config: dict, embedding_model: BaseEmbeddingModel = None) -> BaseEmbeddingModel: + if "embedding_model" in config: + embedding_model_config = config.pop("embedding_model") + embedding_model = self.init_embedding_model(embedding_model_config) + elif embedding_model is None: + raise RuntimeError("embedding_model must be provided.") + return embedding_model + + def init_vector_store(self, vector_store_config: dict) -> BaseVectorStore: + backend = vector_store_config.pop("backend", None) + assert backend is not None, "vector_store must have a backend like `elasticsearch`." + assert backend in VECTOR_STORE_REGISTRY, f"vector_store backend={backend} not supported. " \ + f"supported={VECTOR_STORE_REGISTRY.registered_modules}" + embedding_model = self.get_embedding_model(vector_store_config, embedding_model=self.embedding_model) + vector_store = VECTOR_STORE_REGISTRY[backend](**vector_store_config, embedding_model=embedding_model) + logger.info(f"vector_store is inited with backend={backend} params={vector_store_config}") + return vector_store + + def get_vector_store(self, config: dict, vector_store: BaseVectorStore = None) -> BaseVectorStore: + if "vector_store" in config: + vector_store_config = config.pop("vector_store") + vector_store = self.init_vector_store(vector_store_config) + elif vector_store is None: + raise RuntimeError("vector_store must be provided.") + return vector_store + + def init_context_generator(self, context_generator_config: dict) -> BaseContextGenerator: + backend = context_generator_config.pop("backend", None) + assert backend is not None, "context_generator must have a backend like `simple`." + assert backend in CONTEXT_GENERATOR_REGISTRY, f"context_generator backend={backend} not supported. " \ + f"supported={CONTEXT_GENERATOR_REGISTRY.registered_modules}" + llm = self.get_llm(context_generator_config, llm=self.llm) + vector_store = self.get_vector_store(context_generator_config, vector_store=self.vector_store) + context_generator: BaseContextGenerator = CONTEXT_GENERATOR_REGISTRY[backend]( + **context_generator_config, llm=llm, vector_store=vector_store) + logger.info(f"context_generator is inited with backend={backend} params={context_generator_config}") + return context_generator + + def init_summarizer(self, summarizer_config: dict) -> BaseSummarizer: + backend = summarizer_config.pop("backend", None) + assert backend is not None, "summarizer must have a backend like `simple`." + assert backend in SUMMARIZER_REGISTRY, f"summarizer backend={backend} not supported. " \ + f"supported={SUMMARIZER_REGISTRY.registered_modules}" + llm = self.get_llm(summarizer_config, llm=self.llm) + vector_store = self.get_vector_store(summarizer_config, vector_store=self.vector_store) + summarizer: BaseSummarizer = SUMMARIZER_REGISTRY[backend](**summarizer_config, + llm=llm, vector_store=vector_store) + logger.info(f"summarizer is inited with backend={backend} params={summarizer_config}") + return summarizer + + def init_agent_wrapper(self, agent_wrapper_config: dict) -> AgentWrapperMixin: + backend = agent_wrapper_config.pop("backend", None) + assert backend is not None, "agent_wrapper must have a backend like `simple`." + assert backend in AGENT_WRAPPER_REGISTRY, f"agent_wrapper backend={backend} not supported. " \ + f"supported={AGENT_WRAPPER_REGISTRY.registered_modules}" + + llm = self.get_llm(agent_wrapper_config, llm=self.llm) + + agent_wrapper: AgentWrapperMixin = AGENT_WRAPPER_REGISTRY[backend]( + **agent_wrapper_config, llm=llm, context_generator=self.context_generator) + logger.info(f"agent_wrapper is inited with backend={backend} params={agent_wrapper_config}") + return agent_wrapper + + @model_validator(mode="after") + def init_modules(self): + if self.llm_config: + self.llm = self.init_llm(self.llm_config) + + if self.embedding_model_config: + self.embedding_model = self.init_embedding_model(self.embedding_model_config) + + if self.vector_store_config: + self.vector_store = self.init_vector_store(self.vector_store_config) + + if self.context_generator_config: + self.context_generator = self.init_context_generator(self.context_generator_config) + + if self.summarizer_config: + self.summarizer = self.init_summarizer(self.summarizer_config) + + if self.agent_wrapper_config: + self.agent_wrapper = self.init_agent_wrapper(self.agent_wrapper_config) + + def call_agent_wrapper(self, request: AgentWrapperRequest) -> AgentWrapperResponse: + assert self.agent_wrapper is not None, "agent_wrapper must be provided." + trajectory: Trajectory = self.agent_wrapper.execute(request.query, **request.metadata) + return AgentWrapperResponse(trajectory=trajectory) + + def call_context_generator(self, request: ContextGeneratorRequest) -> ContextGeneratorResponse: + assert self.context_generator is not None, "context_generator must be provided." + context_msg: ContextMessage = self.context_generator.execute(request.trajectory, **request.metadata) + return ContextGeneratorResponse(context_msg=context_msg) + + def call_summarizer(self, request: SummarizerRequest) -> SummarizerResponse: + assert self.summarizer is not None, "summarizer must be provided." + experiences: List[Experience] = self.summarizer.execute(request.trajectories, request.return_experience, + **request.metadata) + return SummarizerResponse(experiences=experiences) diff --git a/experiencemaker/service/http_service.py b/experiencemaker/service/http_service.py new file mode 100644 index 00000000..155df9c0 --- /dev/null +++ b/experiencemaker/service/http_service.py @@ -0,0 +1,50 @@ +import argparse +import json + +import uvicorn +from fastapi import FastAPI + +from experiencemaker.schema.request import AgentWrapperRequest, ContextGeneratorRequest, SummarizerRequest +from experiencemaker.schema.response import AgentWrapperResponse, ContextGeneratorResponse, SummarizerResponse +from experiencemaker.service.experience_maker_service import ExperienceMakerService +from experiencemaker.utils.file_handler import FileHandler + +app = FastAPI() +service: ExperienceMakerService | None = None + +@app.post('/agent_wrapper', response_model=AgentWrapperResponse) +def call_agent_wrapper(request: AgentWrapperRequest): + return service.call_agent_wrapper(request) + + +@app.post('/context_generator', response_model=ContextGeneratorResponse) +def call_context_generator(request: ContextGeneratorRequest): + return service.call_context_generator(request) + + +@app.post('/summarizer', response_model=SummarizerResponse) +def call_summarizer(request: SummarizerRequest): + return service.call_summarizer(request) + + +# launch with: python -m experiencemaker.service.http_service + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=str, help='config dict') + parser.add_argument('--config_path', type=str, help='config load path') + args = parser.parse_args() + + if args.config_path: + config = FileHandler(file_path=args.config_path).load() + elif args.config: + config = json.loads(args.config) + else: + raise RuntimeError("both config and config_path are not specified") + + service = ExperienceMakerService(**config) + uvicorn.run(app, + host=service.host, + port=service.port, + timeout_keep_alive=service.timeout_keep_alive, + limit_concurrency=service.limit_concurrency)