diff --git a/cookbook/en/deployment/agent_app.md b/cookbook/en/deployment/agent_app.md index ef4c978..8cb6bb7 100644 --- a/cookbook/en/deployment/agent_app.md +++ b/cookbook/en/deployment/agent_app.md @@ -85,6 +85,96 @@ agentApp.run("localhost", 10001); ------ + + + +## Custom endpoint + +**Feature** + +Allows the injection of custom endpoints when starting the 'AgentApp'. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok().body("OK")); +agentApp.run("localhost", 10001); +``` + +**Notes** + +- The incoming lambda will act directly on Spring's 'RouterFunction', route according to the http method, the input parameter is limited to ServerRequest, and the return value is limited to ServerResponse. + +------ + +## Lifecycle hooks + +**Feature** + +Allows before and after launching the 'AgentApp'; before and after destroying 'AgentApp'; Exit 'jvm' and do the corresponding processing. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + System.out.println("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + System.out.println("afterRun"); + } +}); +agentApp.run("localhost", 10001); +``` + +**Notes** + +- The incoming AbstractAppLifecycleHook implementation class will act directly on the 'AgentApp' and sort by weight (natural order by integer), and pass in the 'HookContext' (wrapping 'AgentApp', 'DeployManager') instance for processing, and the context's extraInfo is used to pass cross-method, cross-thread variables.------ + + +## middleware + +**Feature** + +Allows the injection of Filter in the spring context when starting 'AgentApp'. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); +agentApp.middleware(filterRegistrationBean); +agentApp.run("localhost", 10001); + + +public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } +} +``` + +**Notes** + +- Based on the servlet specification, it will directly act on the spring context object associated with 'AgentApp' for authentication, request counting, etc., and webflux is not supported at this time. +------ + + ## A2A Streaming Output (SSE) **Features** diff --git a/cookbook/zh/deployment/agent_app.md b/cookbook/zh/deployment/agent_app.md index d8992b9..f737c43 100644 --- a/cookbook/zh/deployment/agent_app.md +++ b/cookbook/zh/deployment/agent_app.md @@ -83,6 +83,98 @@ agentApp.run("localhost", 10001); ------ + +## 自定义服务端点 + +**功能** + +允许在启动 `AgentApp` 时注入自定义端点。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok().body("OK")); +agentApp.run("localhost", 10001); +``` + +**说明** + +- 传入的 lambda 将直接作用于 Spring 的 `RouterFunction`,根据http method的不同进行route,入参限制为ServerRequest,返回值限制为ServerResponse。 + +------ + +## 生命周期钩子 + +**功能** + +允许在启动 `AgentApp` 前、后;销毁`AgentApp`前、后;退出`jvm`后做对应处理。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + System.out.println("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + System.out.println("afterRun"); + } +}); +agentApp.run("localhost", 10001); +``` + +**说明** + +- 传入的 AbstractAppLifecycleHook实现类 将直接作用于 `AgentApp`并按权重(整数自然排序),并传入`HookContext`(包装`AgentApp`、`DeployManager`)实例进行处理,context的extraInfo用于传递跨方法、跨线程变量。 +------ + + +## 中间件 + +**功能** + +允许在启动 `AgentApp` 前往spring上下文注入Filter。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); +agentApp.middleware(filterRegistrationBean); +agentApp.run("localhost", 10001); + + +public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } +} +``` + +**说明** + +- 基于servlet规范,将直接作用于 `AgentApp`关联的spring上下文对象,用于鉴权、请求计数等,目前暂不支持webflux。 +------ + + ## A2A 流式输出(SSE) **功能** diff --git a/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java b/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java index aa4b529..57c5e2c 100644 --- a/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java +++ b/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java @@ -15,6 +15,8 @@ */ package io.agentscope.runtime.engine; + public interface DeployManager { void deploy(Runner runner); + void undeploy(); } diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java new file mode 100644 index 0000000..a45c49f --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; + +import org.springframework.web.servlet.function.ServerResponse; + +public class AgentScopeDeployWithCustomizeEndpointExample { + + private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + app.endpoint("/test/post", List.of("POST"),serverRequest -> { + String time = sdf.format(new Date()); + return ServerResponse.ok().body(String.format("time=%s,post successful",time)); + }); + app.endpoint("/test/get", List.of("GET"),serverRequest -> { + String time = sdf.format(new Date()); + return ServerResponse.ok().body(String.format("time=%s,get successful",time)); + }); + app.run(); + } +} diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java new file mode 100644 index 0000000..d389091 --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope; + +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.hook.AbstractAppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AgentScopeDeployWithLifecycleHookExample { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeDeployWithLifecycleHookExample.class); + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + app.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + LOG.info("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + LOG.info("afterRun"); + } + + @Override + public void onJvmExit(HookContext context) { + LOG.info("onJvmExit"); + } + }); + app.run(); + } +} diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java new file mode 100644 index 0000000..a57dc0b --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; + +public class AgentScopeDeployWithMiddlewareExample { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeDeployWithMiddlewareExample.class); + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); + app.middleware(filterRegistrationBean); + app.run(); + } + + + + public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } + } +} diff --git a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java index fe509a1..ef23374 100644 --- a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java +++ b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java @@ -15,24 +15,32 @@ */ package io.agentscope.runtime; +import io.agentscope.runtime.app.AgentApp; import io.agentscope.runtime.autoconfigure.DeployProperties; import io.agentscope.runtime.engine.DeployManager; import io.agentscope.runtime.engine.Runner; import io.agentscope.runtime.protocol.Protocol; import io.agentscope.runtime.protocol.ProtocolConfig; +import jakarta.servlet.Filter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; import java.util.HashMap; import java.util.List; @@ -40,149 +48,211 @@ import java.util.function.Consumer; public class LocalDeployManager implements DeployManager { - private static final Logger logger = LoggerFactory.getLogger(LocalDeployManager.class); - - private ConfigurableApplicationContext applicationContext; - - private final String endpointName; - private final String host; - private final int port; - private final List protocols; - private final List protocolConfigs; - private final Consumer corsConfigurer; - - private LocalDeployManager(LocalDeployerManagerBuilder builder) { - this.endpointName = builder.endpointName; - this.host = builder.host; - this.port = builder.port; - this.protocols = builder.protocols; - this.protocolConfigs = builder.protocolConfigs; - this.corsConfigurer = builder.corsConfigurer; - } - - @Override - public synchronized void deploy(Runner runner) { - if (this.applicationContext != null && this.applicationContext.isActive()) { - logger.info("Application context is already active, skipping deployment"); - return; - } - - Map serverProps = new HashMap<>(); - if (this.port > 0) { - serverProps.put("server.port", this.port); - } - if (this.host != null && !this.host.isBlank()) { - serverProps.put("server.address", this.host); - } - - logger.info("Starting streaming deployment for endpoint: {}", endpointName); - - this.applicationContext = new SpringApplicationBuilder() - .sources(LocalDeployConfig.class) - .web(WebApplicationType.SERVLET) - .properties(serverProps) - .initializers((GenericApplicationContext ctx) -> { - // Register Runner instance as a bean - ctx.registerBean(Runner.class, () -> runner); - // Register DeployProperties instance as a bean - ctx.registerBean(DeployProperties.class, () -> new DeployProperties(port, host, endpointName)); - // Scan additional packages based on protocols - ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); - scanner.scan("io.agentscope.runtime.lifecycle"); - Map protocolConfigMap = null != protocolConfigs ? protocolConfigs.stream() - .collect(HashMap::new, (map, config) -> map.put(config.type(), config), HashMap::putAll) - : Map.of(); - for (Protocol protocol : protocols) { - String packageName = "io.agentscope.runtime.protocol." + protocol.name().toLowerCase(); - scanner.scan(packageName); - if (protocolConfigMap.containsKey(protocol)) { - ProtocolConfig protocolConfig = protocolConfigMap.get(protocol); - ctx.registerBean(protocolConfig.name(), ProtocolConfig.class, () -> protocolConfig); - } - } - if (corsConfigurer != null) { - ctx.registerBean(WebMvcConfigurer.class, () -> new WebMvcConfigurer() { - @Override - public void addCorsMappings(@NotNull CorsRegistry registry) { - corsConfigurer.accept(registry); - } - }); - } - - }) - .run(); - - logger.info("Streaming deployment completed for endpoint: {}", endpointName); - } - - /** - * Shutdown the application and clean up resources - */ - public synchronized void shutdown() { - if (this.applicationContext != null && this.applicationContext.isActive()) { - logger.info("Shutting down LocalDeployManager..."); - this.applicationContext.close(); - this.applicationContext = null; - logger.info("LocalDeployManager shutdown completed"); - } - } - - /** - * Configuration class for local deployment of streaming services. - * This class enables component scanning for A2A controllers and other Spring components. - */ - @Configuration - @EnableAutoConfiguration - @ComponentScan(basePackages = { - "io.agentscope.runtime.autoconfigure" - }) - public static class LocalDeployConfig { - } - - public static LocalDeployerManagerBuilder builder(){ - return new LocalDeployerManagerBuilder(); - } - - public static class LocalDeployerManagerBuilder { - private String endpointName; - private String host; - private int port = 8080; - private List protocols = List.of(Protocol.A2A, Protocol.ResponseAPI); - private List protocolConfigs = List.of(); - private Consumer corsConfigurer; - - public LocalDeployerManagerBuilder endpointName(String endpointName) { - this.endpointName = endpointName; - return this; - } - - public LocalDeployerManagerBuilder host(String host) { - this.host = host; - return this; - } - - public LocalDeployerManagerBuilder port(int port) { - this.port = port; - return this; - } - - public LocalDeployerManagerBuilder protocols(List protocols) { - this.protocols = protocols; - return this; - } - - public LocalDeployerManagerBuilder protocolConfigs(List protocolConfigs) { - this.protocolConfigs = protocolConfigs; - return this; - } - - public LocalDeployerManagerBuilder corsConfigurer(Consumer corsConfigurer) { - this.corsConfigurer = corsConfigurer; - return this; - } - - public LocalDeployManager build() { - return new LocalDeployManager(this); - } - } + private static final Logger logger = LoggerFactory.getLogger(LocalDeployManager.class); + + private ConfigurableApplicationContext applicationContext; + + private final String endpointName; + private final String host; + private final int port; + private final List protocols; + private final List protocolConfigs; + private final Consumer corsConfigurer; + private final List customEndpoints; + private final List> middlewares; + + + private LocalDeployManager(LocalDeployerManagerBuilder builder) { + this.endpointName = builder.endpointName; + this.host = builder.host; + this.port = builder.port; + this.protocols = builder.protocols; + this.protocolConfigs = builder.protocolConfigs; + this.corsConfigurer = builder.corsConfigurer; + this.customEndpoints = builder.customEndpoints; + this.middlewares = builder.middlewares; + } + + @Override + public synchronized void deploy(Runner runner) { + if (this.applicationContext != null && this.applicationContext.isActive()) { + logger.info("Application context is already active, skipping deployment"); + return; + } + + Map serverProps = new HashMap<>(); + if (this.port > 0) { + serverProps.put("server.port", this.port); + } + if (this.host != null && !this.host.isBlank()) { + serverProps.put("server.address", this.host); + } + + logger.info("Starting streaming deployment for endpoint: {}", endpointName); + + this.applicationContext = new SpringApplicationBuilder() + .sources(LocalDeployConfig.class) + .web(WebApplicationType.SERVLET) + .properties(serverProps) + .initializers((GenericApplicationContext ctx) -> { + // Register Runner instance as a bean + ctx.registerBean(Runner.class, () -> runner); + // Register DeployProperties instance as a bean + ctx.registerBean(DeployProperties.class, () -> new DeployProperties(port, host, endpointName)); + // Scan additional packages based on protocols + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); + scanner.scan("io.agentscope.runtime.lifecycle"); + Map protocolConfigMap = null != protocolConfigs ? protocolConfigs.stream() + .collect(HashMap::new, (map, config) -> map.put(config.type(), config), HashMap::putAll) + : Map.of(); + for (Protocol protocol : protocols) { + String packageName = "io.agentscope.runtime.protocol." + protocol.name().toLowerCase(); + scanner.scan(packageName); + if (protocolConfigMap.containsKey(protocol)) { + ProtocolConfig protocolConfig = protocolConfigMap.get(protocol); + ctx.registerBean(protocolConfig.name(), ProtocolConfig.class, () -> protocolConfig); + } + } + if (corsConfigurer != null) { + ctx.registerBean(WebMvcConfigurer.class, () -> new WebMvcConfigurer() { + @Override + public void addCorsMappings(@NotNull CorsRegistry registry) { + corsConfigurer.accept(registry); + } + }); + } + if (customEndpoints != null && !customEndpoints.isEmpty()) { + RouterFunction routerFunction = routerFunction(); + ctx.registerBean(routerFunction.getClass(), routerFunction); + } + if (middlewares != null && !middlewares.isEmpty()) { + for (FilterRegistrationBean bean : middlewares) { + ctx.registerBean(bean.getFilterName(), FilterRegistrationBean.class, () -> bean); + } + } + }) + .run(); + logger.info("Streaming deployment completed for endpoint: {}", endpointName); + } + + + protected RouterFunction routerFunction() { + RouterFunctions.Builder route = RouterFunctions.route(); + for (AgentApp.EndpointInfo customEndpoint : customEndpoints) { + List methods = customEndpoint.methods; + if (methods == null || methods.isEmpty()) { + continue; + } + if (methods.contains(HttpMethod.GET.name())) { + route.GET(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.HEAD.name())) { + route.HEAD(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.POST.name())) { + route.POST(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.PUT.name())) { + route.PUT(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.DELETE.name())) { + route.DELETE(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.PATCH.name())) { + route.PATCH(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.OPTIONS.name())) { + route.OPTIONS(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + } + return route.build(); + } + + @Override + public void undeploy() { + shutdown(); + } + + /** + * Shutdown the application and clean up resources + */ + public synchronized void shutdown() { + if (this.applicationContext != null && this.applicationContext.isActive()) { + logger.info("Shutting down LocalDeployManager..."); + this.applicationContext.close(); + this.applicationContext = null; + logger.info("LocalDeployManager shutdown completed"); + } + } + + /** + * Configuration class for local deployment of streaming services. + * This class enables component scanning for A2A controllers and other Spring components. + */ + @Configuration + @EnableAutoConfiguration + @ComponentScan(basePackages = { + "io.agentscope.runtime.autoconfigure" + }) + public static class LocalDeployConfig { + } + + public static LocalDeployerManagerBuilder builder() { + return new LocalDeployerManagerBuilder(); + } + + public static class LocalDeployerManagerBuilder { + private String endpointName; + private String host; + private int port = 8080; + private List protocols = List.of(Protocol.A2A, Protocol.ResponseAPI); + private List protocolConfigs = List.of(); + private Consumer corsConfigurer; + private List customEndpoints; + private List> middlewares; + + public LocalDeployerManagerBuilder endpointName(String endpointName) { + this.endpointName = endpointName; + return this; + } + + public LocalDeployerManagerBuilder host(String host) { + this.host = host; + return this; + } + + public LocalDeployerManagerBuilder port(int port) { + this.port = port; + return this; + } + + public LocalDeployerManagerBuilder protocols(List protocols) { + this.protocols = protocols; + return this; + } + + public LocalDeployerManagerBuilder protocolConfigs(List protocolConfigs) { + this.protocolConfigs = protocolConfigs; + return this; + } + + public LocalDeployerManagerBuilder corsConfigurer(Consumer corsConfigurer) { + this.corsConfigurer = corsConfigurer; + return this; + } + + public LocalDeployerManagerBuilder customEndpoints(List customEndpoints) { + this.customEndpoints = customEndpoints; + return this; + } + + public LocalDeployerManagerBuilder middlewares(List> middlewares) { + this.middlewares = middlewares; + return this; + } + + public LocalDeployManager build() { + return new LocalDeployManager(this); + } + } } diff --git a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java index 4f5a45d..25d4ba1 100644 --- a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java +++ b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java @@ -23,11 +23,13 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; @@ -41,19 +43,33 @@ import io.agentscope.runtime.engine.services.memory.persistence.session.InMemorySessionHistoryService; import io.agentscope.runtime.engine.services.memory.service.MemoryService; import io.agentscope.runtime.engine.services.memory.service.SessionHistoryService; +import io.agentscope.runtime.hook.AppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; import io.agentscope.runtime.protocol.ProtocolConfig; import io.agentscope.runtime.sandbox.manager.ManagerConfig; import io.agentscope.runtime.sandbox.manager.SandboxService; +import io.agentscope.runtime.sandbox.manager.client.container.BaseClientStarter; +import io.agentscope.runtime.sandbox.manager.client.container.docker.DockerClientStarter; +import jakarta.servlet.Filter; import okio.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; import picocli.CommandLine; import picocli.CommandLine.Option; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_STOP; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_STOP; +import static io.agentscope.runtime.hook.AppLifecycleHook.JVM_EXIT; + /** * AgentApp class represents an application that runs as an agent. @@ -84,6 +100,7 @@ public class AgentApp { private AgentHandler adapter; private volatile Runner runner; private DeployManager deployManager; + private volatile HookContext hookContext; // Configuration private String endpointPath; @@ -92,8 +109,11 @@ public class AgentApp { private boolean stream = true; private String responseType = "sse"; private Consumer corsConfigurer; + private final List> middlewares = new ArrayList<>(); - private List customEndpoints = new ArrayList<>(); + private final List customEndpoints = new ArrayList<>(); + private final List hooks = new ArrayList<>(); + private final AtomicBoolean stopped = new AtomicBoolean(false); private List protocolConfigs; private static final String DEFAULT_ENV_FILE_PATH = ".env"; private static final String CLASS_SUFFIX = ".class"; @@ -158,7 +178,8 @@ public AgentApp(String[] args) { StateService stateService = component(properties, STATE_SERVICE_PROVIDER, StateServiceProvider.class, new InMemoryStateService()); SessionHistoryService sessionHistoryService = component(properties, SESSION_HISTORY_SERVICE_PROVIDER, SessionHistoryServiceProvider.class, new InMemorySessionHistoryService()); MemoryService memoryService = component(properties, MEMORY_SERVICE_PROVIDER, MemoryServiceProvider.class, new InMemoryMemoryService()); - SandboxService sandboxService = component(properties, SANDBOX_SERVICE_PROVIDER, SandboxServiceProvider.class, new SandboxService(ManagerConfig.builder().build())); + BaseClientStarter clientConfig = DockerClientStarter.builder().build(); + SandboxService sandboxService = component(properties, SANDBOX_SERVICE_PROVIDER, SandboxServiceProvider.class, new SandboxService(ManagerConfig.builder().clientStarter(clientConfig).build())); ServiceComponentManager serviceComponentManager = new ServiceComponentManager(); serviceComponentManager.setStateService(stateService); serviceComponentManager.setSessionHistoryService(sessionHistoryService); @@ -445,21 +466,34 @@ public void run(String host, int port) { .endpointName(endpointPath) .protocolConfigs(protocolConfigs) .corsConfigurer(corsConfigurer) + .customEndpoints(customEndpoints) + .middlewares(middlewares) .build(); } buildRunner(); logger.info("[AgentApp] Starting AgentApp with endpoint: {}, host: {}, port: {}", endpointPath, host, port); - + this.hookContext = new HookContext(this,deployManager); + List lifecycleHooks = hooks.stream() + .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> lifecycleHooks.stream().filter(e -> ((JVM_EXIT & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.onJvmExit(hookContext)))); + lifecycleHooks.stream() + .filter(e -> ((BEFORE_RUN & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.beforeRun(hookContext)); // Deploy via DeployManager deployManager.deploy(runner); logger.info("[AgentApp] AgentApp started successfully on {}:{}{}", host, port, endpointPath); + lifecycleHooks.stream() + .filter(e -> ((AFTER_RUN & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.afterRun(hookContext)); } /** * Register custom endpoint. */ - public AgentApp endpoint(String path, List methods, Function, Object> handler) { + public AgentApp endpoint(String path, List methods, Function handler) { if (methods == null || methods.isEmpty()) { methods = Arrays.asList("POST"); } @@ -473,6 +507,22 @@ public AgentApp endpoint(String path, List methods, Function filter) { + this.middlewares.add(filter); + return this; + } + + public List> middlewares() { + return middlewares; + } + + public List hooks() { + return hooks; + } + // Getters and setters public String getEndpointPath() { return endpointPath; @@ -510,12 +560,14 @@ public List getCustomEndpoints() { return customEndpoints; } + + /** * Endpoint information. */ public static class EndpointInfo { public String path; - public Function, Object> handler; + public Function handler; public List methods; } @@ -613,5 +665,43 @@ public void setSandboxService(SandboxService sandboxService) { this.sandboxService = sandboxService; } } + + public void stop() { + if (stopped.get()) { + return; + } + synchronized (stopped) { + if (stopped.get()) { + return; + } + if (Objects.nonNull(deployManager)) { + List lifecycleHooks = hooks.stream() + .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); + if (Objects.nonNull(hookContext)){ + lifecycleHooks.stream() + .filter(e -> ((BEFORE_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.beforeStop(hookContext)); + } + deployManager.undeploy(); + stopped.set(true); + if (Objects.nonNull(hookContext)){ + lifecycleHooks.stream() + .filter(e -> ((AFTER_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.afterStop(hookContext)); + } + } + } + } + + public AgentApp hooks(AppLifecycleHook... hooks) { + if (Objects.nonNull(hooks)) { + for (AppLifecycleHook hook : hooks) { + if (!this.hooks.contains(hook)) { + this.hooks.add(hook); + } + } + } + return this; + } } diff --git a/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java new file mode 100644 index 0000000..e5de914 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.hook; + +public abstract class AbstractAppLifecycleHook implements AppLifecycleHook { + + protected int operation; + + public AbstractAppLifecycleHook(){ + this.operation = ALL; + } + +} diff --git a/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java new file mode 100644 index 0000000..ca86af7 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.hook; + +public interface AppLifecycleHook { + + int BEFORE_RUN = 1; + + int AFTER_RUN = 1 << 1; + + int BEFORE_STOP = 1 << 2; + + int AFTER_STOP = 1 << 3; + + int JVM_EXIT = 1 << 4; + + int ALL = BEFORE_RUN | AFTER_RUN | BEFORE_STOP | AFTER_STOP | JVM_EXIT; + + default void beforeRun(HookContext context) { + + } + + default void afterRun(HookContext context) { + + } + + default void beforeStop(HookContext context) { + + } + + default void afterStop(HookContext context) { + + } + + default void onJvmExit(HookContext context) { + + } + + default int priority() { + return 0; + } + + int operation(); +} diff --git a/web/src/main/java/io/agentscope/runtime/hook/HookContext.java b/web/src/main/java/io/agentscope/runtime/hook/HookContext.java new file mode 100644 index 0000000..d4aab33 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/HookContext.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package io.agentscope.runtime.hook; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.engine.DeployManager; + +public class HookContext { + private final AgentApp app; + private final DeployManager deployManager; + private final Map extraInfo = new ConcurrentHashMap<>(); + + public HookContext(AgentApp app, DeployManager deployManager) { + this.app = app; + this.deployManager = deployManager; + } + + public AgentApp getApp() { + return app; + } + + public DeployManager getDeployManager() { + return deployManager; + } + + public Map getExtraInfo() { + return extraInfo; + } +} diff --git a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java new file mode 100644 index 0000000..c7f5014 --- /dev/null +++ b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.deployer; + +import java.io.IOException; +import java.net.URI; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import io.agentscope.runtime.adapters.AgentHandler; +import io.agentscope.runtime.adapters.agentscope.MyAgentScopeAgentHandler; +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.hook.AbstractAppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; +import io.agentscope.runtime.sandbox.manager.ManagerConfig; +import io.agentscope.runtime.sandbox.manager.SandboxService; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.web.servlet.function.ServerResponse; + +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_STOP; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_STOP; + +public class AgentScopeDeployTests { + + private static AgentApp app; + + private static AtomicInteger flag; + + private static UUID uuid; + + @BeforeAll + static void setUp() { + System.setProperty("agent.app.port", "7778"); + System.setProperty("agent.app.handler.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MyAgentHandlerProvider"); + System.setProperty("agent.app.sandbox.service.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MySandboxServiceProvider"); + System.setProperty("AI_DASHSCOPE_API_KEY", "your key"); + AgentScopeDeployTests.app = new AgentApp(new String[0]); + flag = new AtomicInteger(0); + app.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return ALL; + } + + @Override + public void beforeRun(HookContext context) { + flag.set(flag.get() | BEFORE_RUN); + } + + @Override + public void afterRun(HookContext context) { + flag.set(flag.get() | AFTER_RUN); + } + + @Override + public void beforeStop(HookContext context) { + flag.set(flag.get() | BEFORE_STOP); + } + + @Override + public void afterStop(HookContext context) { + flag.set(flag.get() | AFTER_STOP); + } + }); + uuid = UUID.randomUUID(); + AgentScopeDeployTests.app.endpoint("/test/get", List.of("GET"), serverRequest -> ServerResponse.ok() + .body(uuid.toString())); + AgentScopeDeployTests.app.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok() + .body("OK")); + FilterRegistrationBean filterRegistrationBean = authFilter(); + AgentScopeDeployTests.app.middleware(filterRegistrationBean); + AgentScopeDeployTests.app.run(); + + } + + @NotNull + private static FilterRegistrationBean authFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter((servletRequest, servletResponse, filterChain) -> { + HttpServletRequest request = (HttpServletRequest) servletRequest; + String token = request.getHeader("token"); + if (Objects.isNull(token) || !token.equals("token")) { + servletResponse.getWriter().write("Unauthorized"); + return; + } + filterChain.doFilter(servletRequest, servletResponse); + }); + filterRegistrationBean.setBeanName("authFilter"); + filterRegistrationBean.setOrder(-1); + filterRegistrationBean.setUrlPatterns(List.of("/test/post")); + return filterRegistrationBean; + } + + @Test + void testHooks() { + Assertions.assertTrue((flag.get() & BEFORE_RUN) > 0); + Assertions.assertTrue((flag.get() & AFTER_RUN) > 0); + } + + @Test + void testCustomizeEndpoint() throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7778/test/get")) + .GET() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals(response.body(), uuid.toString()); + + } + + @Test + void testMiddleware() throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest requestWithoutToken = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7778/test/post")) + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + HttpResponse response1 = client.send(requestWithoutToken, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals("Unauthorized", response1.body()); + + HttpRequest requestWithToken = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7778/test/post")) + .header("token", "token") // add token to header + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + + HttpResponse response2 = client.send(requestWithToken, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals("OK", response2.body()); + + } + + @Test + void testShutdown() { + app.stop(); + Assertions.assertTrue((flag.get() & BEFORE_STOP) > 0); + Assertions.assertTrue((flag.get() & AFTER_STOP) > 0); + } + + public static class MyAgentHandlerProvider implements AgentApp.AgentHandlerProvider { + + @Override + public AgentHandler get(Properties properties, AgentApp.ServiceComponentManager serviceComponentManager) { + MyAgentScopeAgentHandler handler = new MyAgentScopeAgentHandler(); + handler.setStateService(serviceComponentManager.getStateService()); + handler.setSandboxService(serviceComponentManager.getSandboxService()); + handler.setMemoryService(serviceComponentManager.getMemoryService()); + handler.setSessionHistoryService(serviceComponentManager.getSessionHistoryService()); + return handler; + } + } + + public static class MySandboxServiceProvider implements AgentApp.SandboxServiceProvider { + @Override + public SandboxService get(Properties properties) { + ManagerConfig managerConfig = ManagerConfig.builder() + .build(); + return new SandboxService( + managerConfig); + } + } +}