Repository: LnYo-Cly/ai4j
Branch: main
Commit: a5934b569eef
Files: 1275
Total size: 5.4 MB
Directory structure:
gitextract_x29x44af/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── docs-build.yml
│ └── docs-pages.yml
├── .gitignore
├── LICENSE
├── README-EN.md
├── README.md
├── ai4j/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── io/
│ │ │ └── github/
│ │ │ └── lnyocly/
│ │ │ └── ai4j/
│ │ │ ├── agentflow/
│ │ │ │ ├── AgentFlow.java
│ │ │ │ ├── AgentFlowConfig.java
│ │ │ │ ├── AgentFlowException.java
│ │ │ │ ├── AgentFlowType.java
│ │ │ │ ├── AgentFlowUsage.java
│ │ │ │ ├── chat/
│ │ │ │ │ ├── AgentFlowChatEvent.java
│ │ │ │ │ ├── AgentFlowChatListener.java
│ │ │ │ │ ├── AgentFlowChatRequest.java
│ │ │ │ │ ├── AgentFlowChatResponse.java
│ │ │ │ │ ├── AgentFlowChatService.java
│ │ │ │ │ ├── CozeAgentFlowChatService.java
│ │ │ │ │ └── DifyAgentFlowChatService.java
│ │ │ │ ├── support/
│ │ │ │ │ └── AgentFlowSupport.java
│ │ │ │ ├── trace/
│ │ │ │ │ ├── AgentFlowTraceContext.java
│ │ │ │ │ └── AgentFlowTraceListener.java
│ │ │ │ └── workflow/
│ │ │ │ ├── AgentFlowWorkflowEvent.java
│ │ │ │ ├── AgentFlowWorkflowListener.java
│ │ │ │ ├── AgentFlowWorkflowRequest.java
│ │ │ │ ├── AgentFlowWorkflowResponse.java
│ │ │ │ ├── AgentFlowWorkflowService.java
│ │ │ │ ├── CozeAgentFlowWorkflowService.java
│ │ │ │ ├── DifyAgentFlowWorkflowService.java
│ │ │ │ └── N8nAgentFlowWorkflowService.java
│ │ │ ├── annotation/
│ │ │ │ ├── FunctionCall.java
│ │ │ │ ├── FunctionParameter.java
│ │ │ │ └── FunctionRequest.java
│ │ │ ├── auth/
│ │ │ │ └── BearerTokenUtils.java
│ │ │ ├── config/
│ │ │ │ ├── AiPlatform.java
│ │ │ │ ├── BaichuanConfig.java
│ │ │ │ ├── DashScopeConfig.java
│ │ │ │ ├── DeepSeekConfig.java
│ │ │ │ ├── DoubaoConfig.java
│ │ │ │ ├── HunyuanConfig.java
│ │ │ │ ├── JinaConfig.java
│ │ │ │ ├── LingyiConfig.java
│ │ │ │ ├── McpConfig.java
│ │ │ │ ├── MilvusConfig.java
│ │ │ │ ├── MinimaxConfig.java
│ │ │ │ ├── MoonshotConfig.java
│ │ │ │ ├── OkHttpConfig.java
│ │ │ │ ├── OllamaConfig.java
│ │ │ │ ├── OpenAiConfig.java
│ │ │ │ ├── PgVectorConfig.java
│ │ │ │ ├── PineconeConfig.java
│ │ │ │ ├── QdrantConfig.java
│ │ │ │ └── ZhipuConfig.java
│ │ │ ├── constant/
│ │ │ │ └── Constants.java
│ │ │ ├── convert/
│ │ │ │ ├── audio/
│ │ │ │ │ ├── AudioParameterConvert.java
│ │ │ │ │ └── AudioResultConvert.java
│ │ │ │ ├── chat/
│ │ │ │ │ ├── ParameterConvert.java
│ │ │ │ │ └── ResultConvert.java
│ │ │ │ └── embedding/
│ │ │ │ ├── EmbeddingParameterConvert.java
│ │ │ │ └── EmbeddingResultConvert.java
│ │ │ ├── document/
│ │ │ │ ├── RecursiveCharacterTextSplitter.java
│ │ │ │ └── TikaUtil.java
│ │ │ ├── exception/
│ │ │ │ ├── Ai4jException.java
│ │ │ │ ├── CommonException.java
│ │ │ │ ├── chain/
│ │ │ │ │ ├── AbstractErrorHandler.java
│ │ │ │ │ ├── ErrorHandler.java
│ │ │ │ │ ├── IErrorHandler.java
│ │ │ │ │ └── impl/
│ │ │ │ │ ├── HunyuanErrorHandler.java
│ │ │ │ │ ├── OpenAiErrorHandler.java
│ │ │ │ │ └── UnknownErrorHandler.java
│ │ │ │ └── error/
│ │ │ │ ├── Error.java
│ │ │ │ ├── HunyuanError.java
│ │ │ │ └── OpenAiError.java
│ │ │ ├── interceptor/
│ │ │ │ ├── ContentTypeInterceptor.java
│ │ │ │ └── ErrorInterceptor.java
│ │ │ ├── listener/
│ │ │ │ ├── AbstractManagedStreamListener.java
│ │ │ │ ├── ImageSseListener.java
│ │ │ │ ├── ManagedStreamListener.java
│ │ │ │ ├── RealtimeListener.java
│ │ │ │ ├── ResponseSseListener.java
│ │ │ │ ├── SseListener.java
│ │ │ │ ├── StreamExecutionOptions.java
│ │ │ │ └── StreamExecutionSupport.java
│ │ │ ├── mcp/
│ │ │ │ ├── annotation/
│ │ │ │ │ ├── McpParameter.java
│ │ │ │ │ ├── McpPrompt.java
│ │ │ │ │ ├── McpPromptParameter.java
│ │ │ │ │ ├── McpResource.java
│ │ │ │ │ ├── McpResourceParameter.java
│ │ │ │ │ ├── McpService.java
│ │ │ │ │ └── McpTool.java
│ │ │ │ ├── client/
│ │ │ │ │ ├── McpClient.java
│ │ │ │ │ └── McpClientResponseSupport.java
│ │ │ │ ├── config/
│ │ │ │ │ ├── FileMcpConfigSource.java
│ │ │ │ │ ├── McpConfigIO.java
│ │ │ │ │ ├── McpConfigManager.java
│ │ │ │ │ ├── McpConfigSource.java
│ │ │ │ │ └── McpServerConfig.java
│ │ │ │ ├── entity/
│ │ │ │ │ ├── McpError.java
│ │ │ │ │ ├── McpInitializeResponse.java
│ │ │ │ │ ├── McpMessage.java
│ │ │ │ │ ├── McpNotification.java
│ │ │ │ │ ├── McpPrompt.java
│ │ │ │ │ ├── McpPromptResult.java
│ │ │ │ │ ├── McpRequest.java
│ │ │ │ │ ├── McpResource.java
│ │ │ │ │ ├── McpResourceContent.java
│ │ │ │ │ ├── McpResponse.java
│ │ │ │ │ ├── McpRoot.java
│ │ │ │ │ ├── McpSamplingRequest.java
│ │ │ │ │ ├── McpSamplingResult.java
│ │ │ │ │ ├── McpServerInfo.java
│ │ │ │ │ ├── McpServerReference.java
│ │ │ │ │ ├── McpTool.java
│ │ │ │ │ ├── McpToolDefinition.java
│ │ │ │ │ └── McpToolResult.java
│ │ │ │ ├── gateway/
│ │ │ │ │ ├── McpGateway.java
│ │ │ │ │ ├── McpGatewayClientFactory.java
│ │ │ │ │ ├── McpGatewayConfigSourceBinding.java
│ │ │ │ │ ├── McpGatewayKeySupport.java
│ │ │ │ │ └── McpGatewayToolRegistry.java
│ │ │ │ ├── server/
│ │ │ │ │ ├── McpHttpServerSupport.java
│ │ │ │ │ ├── McpServer.java
│ │ │ │ │ ├── McpServerEngine.java
│ │ │ │ │ ├── McpServerFactory.java
│ │ │ │ │ ├── McpServerSessionState.java
│ │ │ │ │ ├── McpServerSessionSupport.java
│ │ │ │ │ ├── SseMcpServer.java
│ │ │ │ │ ├── StdioMcpServer.java
│ │ │ │ │ └── StreamableHttpMcpServer.java
│ │ │ │ ├── transport/
│ │ │ │ │ ├── McpTransport.java
│ │ │ │ │ ├── McpTransportFactory.java
│ │ │ │ │ ├── McpTransportSupport.java
│ │ │ │ │ ├── SseTransport.java
│ │ │ │ │ ├── StdioTransport.java
│ │ │ │ │ ├── StreamableHttpTransport.java
│ │ │ │ │ └── TransportConfig.java
│ │ │ │ └── util/
│ │ │ │ ├── McpMessageCodec.java
│ │ │ │ ├── McpPromptAdapter.java
│ │ │ │ ├── McpResourceAdapter.java
│ │ │ │ ├── McpToolAdapter.java
│ │ │ │ ├── McpToolConversionSupport.java
│ │ │ │ └── McpTypeSupport.java
│ │ │ ├── memory/
│ │ │ │ ├── ChatMemory.java
│ │ │ │ ├── ChatMemoryItem.java
│ │ │ │ ├── ChatMemoryPolicy.java
│ │ │ │ ├── ChatMemorySnapshot.java
│ │ │ │ ├── ChatMemorySummarizer.java
│ │ │ │ ├── ChatMemorySummaryRequest.java
│ │ │ │ ├── InMemoryChatMemory.java
│ │ │ │ ├── JdbcChatMemory.java
│ │ │ │ ├── JdbcChatMemoryConfig.java
│ │ │ │ ├── MessageWindowChatMemoryPolicy.java
│ │ │ │ ├── SummaryChatMemoryPolicy.java
│ │ │ │ ├── SummaryChatMemoryPolicyConfig.java
│ │ │ │ └── UnboundedChatMemoryPolicy.java
│ │ │ ├── network/
│ │ │ │ ├── ConnectionPoolProvider.java
│ │ │ │ ├── DispatcherProvider.java
│ │ │ │ ├── OkHttpUtil.java
│ │ │ │ ├── UrlUtils.java
│ │ │ │ └── impl/
│ │ │ │ ├── DefaultConnectionPoolProvider.java
│ │ │ │ └── DefaultDispatcherProvider.java
│ │ │ ├── platform/
│ │ │ │ ├── baichuan/
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── BaichuanChatService.java
│ │ │ │ │ └── entity/
│ │ │ │ │ ├── BaichuanChatCompletion.java
│ │ │ │ │ └── BaichuanChatCompletionResponse.java
│ │ │ │ ├── dashscope/
│ │ │ │ │ ├── DashScopeChatService.java
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ └── DashScopeResult.java
│ │ │ │ │ ├── response/
│ │ │ │ │ │ └── DashScopeResponsesService.java
│ │ │ │ │ └── util/
│ │ │ │ │ └── MessageUtil.java
│ │ │ │ ├── deepseek/
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── DeepSeekChatService.java
│ │ │ │ │ └── entity/
│ │ │ │ │ ├── DeepSeekChatCompletion.java
│ │ │ │ │ └── DeepSeekChatCompletionResponse.java
│ │ │ │ ├── doubao/
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── DoubaoChatService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── DoubaoChatCompletion.java
│ │ │ │ │ │ └── DoubaoChatCompletionResponse.java
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── DoubaoImageService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ └── DoubaoImageGenerationRequest.java
│ │ │ │ │ ├── rerank/
│ │ │ │ │ │ └── DoubaoRerankService.java
│ │ │ │ │ └── response/
│ │ │ │ │ └── DoubaoResponsesService.java
│ │ │ │ ├── hunyuan/
│ │ │ │ │ ├── HunyuanConstant.java
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── HunyuanChatService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── HunyuanChatCompletion.java
│ │ │ │ │ │ └── HunyuanChatCompletionResponse.java
│ │ │ │ │ └── support/
│ │ │ │ │ └── HunyuanJsonUtil.java
│ │ │ │ ├── jina/
│ │ │ │ │ └── rerank/
│ │ │ │ │ └── JinaRerankService.java
│ │ │ │ ├── lingyi/
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── LingyiChatService.java
│ │ │ │ │ └── entity/
│ │ │ │ │ ├── LingyiChatCompletion.java
│ │ │ │ │ └── LingyiChatCompletionResponse.java
│ │ │ │ ├── minimax/
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── MinimaxChatService.java
│ │ │ │ │ └── entity/
│ │ │ │ │ ├── MinimaxChatCompletion.java
│ │ │ │ │ └── MinimaxChatCompletionResponse.java
│ │ │ │ ├── moonshot/
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── MoonshotChatService.java
│ │ │ │ │ └── entity/
│ │ │ │ │ ├── MoonshotChatCompletion.java
│ │ │ │ │ └── MoonshotChatCompletionResponse.java
│ │ │ │ ├── ollama/
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── OllamaAiChatService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── OllamaChatCompletion.java
│ │ │ │ │ │ ├── OllamaChatCompletionResponse.java
│ │ │ │ │ │ ├── OllamaMessage.java
│ │ │ │ │ │ └── OllamaOptions.java
│ │ │ │ │ ├── embedding/
│ │ │ │ │ │ ├── OllamaEmbeddingService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── OllamaEmbedding.java
│ │ │ │ │ │ └── OllamaEmbeddingResponse.java
│ │ │ │ │ └── rerank/
│ │ │ │ │ └── OllamaRerankService.java
│ │ │ │ ├── openai/
│ │ │ │ │ ├── audio/
│ │ │ │ │ │ ├── OpenAiAudioService.java
│ │ │ │ │ │ ├── entity/
│ │ │ │ │ │ │ ├── Segment.java
│ │ │ │ │ │ │ ├── TextToSpeech.java
│ │ │ │ │ │ │ ├── Transcription.java
│ │ │ │ │ │ │ ├── TranscriptionResponse.java
│ │ │ │ │ │ │ ├── Translation.java
│ │ │ │ │ │ │ ├── TranslationResponse.java
│ │ │ │ │ │ │ └── Word.java
│ │ │ │ │ │ └── enums/
│ │ │ │ │ │ ├── AudioEnum.java
│ │ │ │ │ │ └── WhisperEnum.java
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── OpenAiChatService.java
│ │ │ │ │ │ ├── entity/
│ │ │ │ │ │ │ ├── ChatCompletion.java
│ │ │ │ │ │ │ ├── ChatCompletionResponse.java
│ │ │ │ │ │ │ ├── ChatMessage.java
│ │ │ │ │ │ │ ├── Choice.java
│ │ │ │ │ │ │ ├── Content.java
│ │ │ │ │ │ │ └── StreamOptions.java
│ │ │ │ │ │ ├── enums/
│ │ │ │ │ │ │ └── ChatMessageType.java
│ │ │ │ │ │ └── serializer/
│ │ │ │ │ │ └── ContentDeserializer.java
│ │ │ │ │ ├── embedding/
│ │ │ │ │ │ ├── OpenAiEmbeddingService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── Embedding.java
│ │ │ │ │ │ ├── EmbeddingObject.java
│ │ │ │ │ │ └── EmbeddingResponse.java
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── OpenAiImageService.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── ImageData.java
│ │ │ │ │ │ ├── ImageGeneration.java
│ │ │ │ │ │ ├── ImageGenerationResponse.java
│ │ │ │ │ │ ├── ImageStreamError.java
│ │ │ │ │ │ ├── ImageStreamEvent.java
│ │ │ │ │ │ ├── ImageUsage.java
│ │ │ │ │ │ └── ImageUsageDetails.java
│ │ │ │ │ ├── realtime/
│ │ │ │ │ │ ├── OpenAiRealtimeService.java
│ │ │ │ │ │ ├── RealtimeConstant.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── ConversationCreated.java
│ │ │ │ │ │ ├── Session.java
│ │ │ │ │ │ ├── SessionCreated.java
│ │ │ │ │ │ └── SessionUpdated.java
│ │ │ │ │ ├── response/
│ │ │ │ │ │ ├── OpenAiResponsesService.java
│ │ │ │ │ │ ├── ResponseEventParser.java
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── ImagePixelLimit.java
│ │ │ │ │ │ ├── Response.java
│ │ │ │ │ │ ├── ResponseContentPart.java
│ │ │ │ │ │ ├── ResponseContextEdit.java
│ │ │ │ │ │ ├── ResponseContextManagement.java
│ │ │ │ │ │ ├── ResponseDeleteResponse.java
│ │ │ │ │ │ ├── ResponseError.java
│ │ │ │ │ │ ├── ResponseIncompleteDetails.java
│ │ │ │ │ │ ├── ResponseItem.java
│ │ │ │ │ │ ├── ResponseRequest.java
│ │ │ │ │ │ ├── ResponseStreamEvent.java
│ │ │ │ │ │ ├── ResponseSummary.java
│ │ │ │ │ │ ├── ResponseToolUsage.java
│ │ │ │ │ │ ├── ResponseToolUsageDetails.java
│ │ │ │ │ │ ├── ResponseUsage.java
│ │ │ │ │ │ ├── ResponseUsageDetails.java
│ │ │ │ │ │ └── TranslationOptions.java
│ │ │ │ │ ├── tool/
│ │ │ │ │ │ ├── Tool.java
│ │ │ │ │ │ └── ToolCall.java
│ │ │ │ │ └── usage/
│ │ │ │ │ └── Usage.java
│ │ │ │ ├── standard/
│ │ │ │ │ └── rerank/
│ │ │ │ │ └── StandardRerankService.java
│ │ │ │ └── zhipu/
│ │ │ │ └── chat/
│ │ │ │ ├── ZhipuChatService.java
│ │ │ │ └── entity/
│ │ │ │ ├── ZhipuChatCompletion.java
│ │ │ │ └── ZhipuChatCompletionResponse.java
│ │ │ ├── rag/
│ │ │ │ ├── AbstractScoreFusionStrategy.java
│ │ │ │ ├── Bm25Retriever.java
│ │ │ │ ├── DbsfFusionStrategy.java
│ │ │ │ ├── DefaultRagContextAssembler.java
│ │ │ │ ├── DefaultRagService.java
│ │ │ │ ├── DefaultTextTokenizer.java
│ │ │ │ ├── DenseRetriever.java
│ │ │ │ ├── FusionStrategy.java
│ │ │ │ ├── HybridRetriever.java
│ │ │ │ ├── ModelReranker.java
│ │ │ │ ├── NoopReranker.java
│ │ │ │ ├── RagChunk.java
│ │ │ │ ├── RagCitation.java
│ │ │ │ ├── RagContext.java
│ │ │ │ ├── RagContextAssembler.java
│ │ │ │ ├── RagDocument.java
│ │ │ │ ├── RagEvaluation.java
│ │ │ │ ├── RagEvaluator.java
│ │ │ │ ├── RagHit.java
│ │ │ │ ├── RagHitSupport.java
│ │ │ │ ├── RagMetadataKeys.java
│ │ │ │ ├── RagQuery.java
│ │ │ │ ├── RagResult.java
│ │ │ │ ├── RagScoreDetail.java
│ │ │ │ ├── RagService.java
│ │ │ │ ├── RagTrace.java
│ │ │ │ ├── Reranker.java
│ │ │ │ ├── Retriever.java
│ │ │ │ ├── RrfFusionStrategy.java
│ │ │ │ ├── RsfFusionStrategy.java
│ │ │ │ ├── TextTokenizer.java
│ │ │ │ └── ingestion/
│ │ │ │ ├── Chunker.java
│ │ │ │ ├── DefaultMetadataEnricher.java
│ │ │ │ ├── DocumentLoader.java
│ │ │ │ ├── IngestionPipeline.java
│ │ │ │ ├── IngestionRequest.java
│ │ │ │ ├── IngestionResult.java
│ │ │ │ ├── IngestionSource.java
│ │ │ │ ├── LoadedDocument.java
│ │ │ │ ├── LoadedDocumentProcessor.java
│ │ │ │ ├── MetadataEnricher.java
│ │ │ │ ├── OcrNoiseCleaningDocumentProcessor.java
│ │ │ │ ├── OcrTextExtractingDocumentProcessor.java
│ │ │ │ ├── OcrTextExtractor.java
│ │ │ │ ├── RecursiveTextChunker.java
│ │ │ │ ├── TextDocumentLoader.java
│ │ │ │ ├── TikaDocumentLoader.java
│ │ │ │ └── WhitespaceNormalizingDocumentProcessor.java
│ │ │ ├── rerank/
│ │ │ │ └── entity/
│ │ │ │ ├── RerankDocument.java
│ │ │ │ ├── RerankRequest.java
│ │ │ │ ├── RerankResponse.java
│ │ │ │ ├── RerankResult.java
│ │ │ │ └── RerankUsage.java
│ │ │ ├── service/
│ │ │ │ ├── AiConfig.java
│ │ │ │ ├── Configuration.java
│ │ │ │ ├── IAudioService.java
│ │ │ │ ├── IChatService.java
│ │ │ │ ├── IEmbeddingService.java
│ │ │ │ ├── IImageService.java
│ │ │ │ ├── IRealtimeService.java
│ │ │ │ ├── IRerankService.java
│ │ │ │ ├── IResponsesService.java
│ │ │ │ ├── ModelType.java
│ │ │ │ ├── PlatformType.java
│ │ │ │ ├── factory/
│ │ │ │ │ ├── AiService.java
│ │ │ │ │ ├── AiServiceFactory.java
│ │ │ │ │ ├── AiServiceRegistration.java
│ │ │ │ │ ├── AiServiceRegistry.java
│ │ │ │ │ ├── DefaultAiServiceFactory.java
│ │ │ │ │ ├── DefaultAiServiceRegistry.java
│ │ │ │ │ └── FreeAiService.java
│ │ │ │ └── spi/
│ │ │ │ └── ServiceLoaderUtil.java
│ │ │ ├── skill/
│ │ │ │ ├── SkillDescriptor.java
│ │ │ │ └── Skills.java
│ │ │ ├── token/
│ │ │ │ └── TikTokensUtil.java
│ │ │ ├── tool/
│ │ │ │ ├── BuiltInProcessRegistry.java
│ │ │ │ ├── BuiltInToolContext.java
│ │ │ │ ├── BuiltInToolExecutor.java
│ │ │ │ ├── BuiltInTools.java
│ │ │ │ ├── ResponseRequestToolResolver.java
│ │ │ │ └── ToolUtil.java
│ │ │ ├── tools/
│ │ │ │ ├── ApplyPatchFunction.java
│ │ │ │ ├── BashFunction.java
│ │ │ │ ├── QueryTrainInfoFunction.java
│ │ │ │ ├── QueryWeatherFunction.java
│ │ │ │ ├── ReadFileFunction.java
│ │ │ │ └── WriteFileFunction.java
│ │ │ ├── vector/
│ │ │ │ ├── VectorDataEntity.java
│ │ │ │ ├── pinecone/
│ │ │ │ │ ├── PineconeDelete.java
│ │ │ │ │ ├── PineconeInsert.java
│ │ │ │ │ ├── PineconeInsertResponse.java
│ │ │ │ │ ├── PineconeQuery.java
│ │ │ │ │ ├── PineconeQueryResponse.java
│ │ │ │ │ └── PineconeVectors.java
│ │ │ │ ├── service/
│ │ │ │ │ └── PineconeService.java
│ │ │ │ └── store/
│ │ │ │ ├── VectorDeleteRequest.java
│ │ │ │ ├── VectorRecord.java
│ │ │ │ ├── VectorSearchRequest.java
│ │ │ │ ├── VectorSearchResult.java
│ │ │ │ ├── VectorStore.java
│ │ │ │ ├── VectorStoreCapabilities.java
│ │ │ │ ├── VectorUpsertRequest.java
│ │ │ │ ├── milvus/
│ │ │ │ │ └── MilvusVectorStore.java
│ │ │ │ ├── pgvector/
│ │ │ │ │ └── PgVectorStore.java
│ │ │ │ ├── pinecone/
│ │ │ │ │ └── PineconeVectorStore.java
│ │ │ │ └── qdrant/
│ │ │ │ └── QdrantVectorStore.java
│ │ │ └── websearch/
│ │ │ ├── ChatWithWebSearchEnhance.java
│ │ │ └── searxng/
│ │ │ ├── SearXNGConfig.java
│ │ │ ├── SearXNGRequest.java
│ │ │ └── SearXNGResponse.java
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── services/
│ │ │ ├── io.github.lnyocly.ai4j.network.ConnectionPoolProvider
│ │ │ └── io.github.lnyocly.ai4j.network.DispatcherProvider
│ │ └── mcp-servers-config.json
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ ├── BaichuanTest.java
│ ├── DashScopeTest.java
│ ├── DeepSeekTest.java
│ ├── DoubaoImageTest.java
│ ├── DoubaoResponsesTest.java
│ ├── DoubaoTest.java
│ ├── HunyuanTest.java
│ ├── LingyiTest.java
│ ├── MinimaxTest.java
│ ├── MoonshotTest.java
│ ├── OllamaTest.java
│ ├── OpenAiTest.java
│ ├── OtherTest.java
│ ├── ZhipuTest.java
│ ├── ai4j/
│ │ ├── agentflow/
│ │ │ ├── AgentFlowTraceSupportTest.java
│ │ │ ├── CozeAgentFlowServiceTest.java
│ │ │ ├── DifyAgentFlowServiceTest.java
│ │ │ └── N8nAgentFlowWorkflowServiceTest.java
│ │ ├── function/
│ │ │ └── TestFunction.java
│ │ ├── mcp/
│ │ │ ├── McpClientResponseSupportTest.java
│ │ │ ├── McpGatewaySupportTest.java
│ │ │ ├── McpServerTest.java
│ │ │ ├── McpTypeSupportTest.java
│ │ │ ├── TestMcpService.java
│ │ │ ├── config/
│ │ │ │ └── McpConfigManagerTest.java
│ │ │ ├── gateway/
│ │ │ │ └── McpGatewayConfigSourceTest.java
│ │ │ ├── transport/
│ │ │ │ └── SseTransportTest.java
│ │ │ └── util/
│ │ │ └── TestMcpService.java
│ │ ├── memory/
│ │ │ ├── InMemoryChatMemoryTest.java
│ │ │ ├── JdbcChatMemoryTest.java
│ │ │ └── SummaryChatMemoryPolicyTest.java
│ │ ├── platform/
│ │ │ ├── doubao/
│ │ │ │ └── rerank/
│ │ │ │ └── DoubaoRerankServiceTest.java
│ │ │ ├── jina/
│ │ │ │ └── rerank/
│ │ │ │ └── JinaRerankServiceTest.java
│ │ │ ├── minimax/
│ │ │ │ └── chat/
│ │ │ │ └── MinimaxChatServiceTest.java
│ │ │ ├── ollama/
│ │ │ │ └── rerank/
│ │ │ │ └── OllamaRerankServiceTest.java
│ │ │ └── openai/
│ │ │ ├── audio/
│ │ │ │ └── OpenAiAudioServiceTest.java
│ │ │ ├── chat/
│ │ │ │ └── OpenAiChatServicePassThroughTest.java
│ │ │ └── response/
│ │ │ └── ResponseRequestToolResolverTest.java
│ │ ├── rag/
│ │ │ ├── Bm25RetrieverTest.java
│ │ │ ├── DefaultRagServiceTest.java
│ │ │ ├── DenseRetrieverTest.java
│ │ │ ├── HybridRetrieverTest.java
│ │ │ ├── IngestionPipelineTest.java
│ │ │ ├── ModelRerankerTest.java
│ │ │ └── RagEvaluatorTest.java
│ │ ├── skill/
│ │ │ └── SkillsIChatServiceTest.java
│ │ ├── tool/
│ │ │ └── BuiltInToolExecutorTest.java
│ │ └── vector/
│ │ └── store/
│ │ ├── milvus/
│ │ │ └── MilvusVectorStoreTest.java
│ │ ├── pinecone/
│ │ │ └── PineconeVectorStoreTest.java
│ │ └── qdrant/
│ │ └── QdrantVectorStoreTest.java
│ ├── interceptor/
│ │ └── ErrorInterceptorTest.java
│ ├── listener/
│ │ ├── SseListenerTest.java
│ │ └── StreamExecutionSupportTest.java
│ └── service/
│ └── AiServiceRegistryTest.java
├── ai4j-agent/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── io/
│ │ └── github/
│ │ └── lnyocly/
│ │ └── ai4j/
│ │ └── agent/
│ │ ├── Agent.java
│ │ ├── AgentBuilder.java
│ │ ├── AgentContext.java
│ │ ├── AgentOptions.java
│ │ ├── AgentRequest.java
│ │ ├── AgentResult.java
│ │ ├── AgentRuntime.java
│ │ ├── AgentSession.java
│ │ ├── Agents.java
│ │ ├── codeact/
│ │ │ ├── CodeActOptions.java
│ │ │ ├── CodeExecutionRequest.java
│ │ │ ├── CodeExecutionResult.java
│ │ │ ├── CodeExecutor.java
│ │ │ ├── GraalVmCodeExecutor.java
│ │ │ └── NashornCodeExecutor.java
│ │ ├── event/
│ │ │ ├── AgentEvent.java
│ │ │ ├── AgentEventPublisher.java
│ │ │ ├── AgentEventType.java
│ │ │ └── AgentListener.java
│ │ ├── flowgram/
│ │ │ ├── Ai4jFlowGramLlmNodeRunner.java
│ │ │ ├── FlowGramLlmNodeRunner.java
│ │ │ ├── FlowGramNodeExecutionContext.java
│ │ │ ├── FlowGramNodeExecutionResult.java
│ │ │ ├── FlowGramNodeExecutor.java
│ │ │ ├── FlowGramRuntimeEvent.java
│ │ │ ├── FlowGramRuntimeListener.java
│ │ │ ├── FlowGramRuntimeService.java
│ │ │ └── model/
│ │ │ ├── FlowGramEdgeSchema.java
│ │ │ ├── FlowGramNodeSchema.java
│ │ │ ├── FlowGramTaskCancelOutput.java
│ │ │ ├── FlowGramTaskReportOutput.java
│ │ │ ├── FlowGramTaskResultOutput.java
│ │ │ ├── FlowGramTaskRunInput.java
│ │ │ ├── FlowGramTaskRunOutput.java
│ │ │ ├── FlowGramTaskValidateOutput.java
│ │ │ └── FlowGramWorkflowSchema.java
│ │ ├── memory/
│ │ │ ├── AgentMemory.java
│ │ │ ├── InMemoryAgentMemory.java
│ │ │ ├── JdbcAgentMemory.java
│ │ │ ├── JdbcAgentMemoryConfig.java
│ │ │ ├── MemoryCompressor.java
│ │ │ ├── MemorySnapshot.java
│ │ │ └── WindowedMemoryCompressor.java
│ │ ├── model/
│ │ │ ├── AgentModelClient.java
│ │ │ ├── AgentModelResult.java
│ │ │ ├── AgentModelStreamListener.java
│ │ │ ├── AgentPrompt.java
│ │ │ ├── ChatModelClient.java
│ │ │ └── ResponsesModelClient.java
│ │ ├── runtime/
│ │ │ ├── AgentToolExecutionScope.java
│ │ │ ├── BaseAgentRuntime.java
│ │ │ ├── CodeActRuntime.java
│ │ │ ├── DeepResearchRuntime.java
│ │ │ ├── Planner.java
│ │ │ └── ReActRuntime.java
│ │ ├── subagent/
│ │ │ ├── HandoffContext.java
│ │ │ ├── HandoffFailureAction.java
│ │ │ ├── HandoffInputFilter.java
│ │ │ ├── HandoffPolicy.java
│ │ │ ├── StaticSubAgentRegistry.java
│ │ │ ├── SubAgentDefinition.java
│ │ │ ├── SubAgentRegistry.java
│ │ │ ├── SubAgentSessionMode.java
│ │ │ └── SubAgentToolExecutor.java
│ │ ├── team/
│ │ │ ├── AgentTeam.java
│ │ │ ├── AgentTeamAgentRuntime.java
│ │ │ ├── AgentTeamBuilder.java
│ │ │ ├── AgentTeamControl.java
│ │ │ ├── AgentTeamEventHook.java
│ │ │ ├── AgentTeamHook.java
│ │ │ ├── AgentTeamMember.java
│ │ │ ├── AgentTeamMemberResult.java
│ │ │ ├── AgentTeamMemberSnapshot.java
│ │ │ ├── AgentTeamMessage.java
│ │ │ ├── AgentTeamMessageBus.java
│ │ │ ├── AgentTeamOptions.java
│ │ │ ├── AgentTeamPlan.java
│ │ │ ├── AgentTeamPlanApproval.java
│ │ │ ├── AgentTeamPlanParser.java
│ │ │ ├── AgentTeamPlanner.java
│ │ │ ├── AgentTeamResult.java
│ │ │ ├── AgentTeamState.java
│ │ │ ├── AgentTeamStateStore.java
│ │ │ ├── AgentTeamSynthesizer.java
│ │ │ ├── AgentTeamTask.java
│ │ │ ├── AgentTeamTaskBoard.java
│ │ │ ├── AgentTeamTaskState.java
│ │ │ ├── AgentTeamTaskStatus.java
│ │ │ ├── FileAgentTeamMessageBus.java
│ │ │ ├── FileAgentTeamStateStore.java
│ │ │ ├── InMemoryAgentTeamMessageBus.java
│ │ │ ├── InMemoryAgentTeamStateStore.java
│ │ │ ├── LlmAgentTeamPlanner.java
│ │ │ ├── LlmAgentTeamSynthesizer.java
│ │ │ └── tool/
│ │ │ ├── AgentTeamToolExecutor.java
│ │ │ └── AgentTeamToolRegistry.java
│ │ ├── tool/
│ │ │ ├── AgentToolCall.java
│ │ │ ├── AgentToolCallSanitizer.java
│ │ │ ├── AgentToolRegistry.java
│ │ │ ├── AgentToolResult.java
│ │ │ ├── CompositeToolRegistry.java
│ │ │ ├── StaticToolRegistry.java
│ │ │ ├── ToolExecutor.java
│ │ │ ├── ToolUtilExecutor.java
│ │ │ └── ToolUtilRegistry.java
│ │ ├── trace/
│ │ │ ├── AbstractOpenTelemetryTraceExporter.java
│ │ │ ├── AgentFlowTraceBridge.java
│ │ │ ├── AgentTraceListener.java
│ │ │ ├── CompositeTraceExporter.java
│ │ │ ├── ConsoleTraceExporter.java
│ │ │ ├── InMemoryTraceExporter.java
│ │ │ ├── JsonlTraceExporter.java
│ │ │ ├── LangfuseTraceExporter.java
│ │ │ ├── OpenTelemetryTraceExporter.java
│ │ │ ├── OpenTelemetryTraceSupport.java
│ │ │ ├── TraceConfig.java
│ │ │ ├── TraceExporter.java
│ │ │ ├── TraceMasker.java
│ │ │ ├── TraceMetrics.java
│ │ │ ├── TracePricing.java
│ │ │ ├── TracePricingResolver.java
│ │ │ ├── TraceSpan.java
│ │ │ ├── TraceSpanEvent.java
│ │ │ ├── TraceSpanStatus.java
│ │ │ └── TraceSpanType.java
│ │ ├── util/
│ │ │ ├── AgentInputItem.java
│ │ │ └── ResponseUtil.java
│ │ └── workflow/
│ │ ├── AgentNode.java
│ │ ├── AgentWorkflow.java
│ │ ├── RuntimeAgentNode.java
│ │ ├── SequentialWorkflow.java
│ │ ├── StateCondition.java
│ │ ├── StateGraphWorkflow.java
│ │ ├── StateRouter.java
│ │ ├── StateTransition.java
│ │ ├── WorkflowAgent.java
│ │ ├── WorkflowContext.java
│ │ └── WorkflowResultAware.java
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ ├── agent/
│ │ ├── AgentFlowTraceBridgeTest.java
│ │ ├── AgentMemoryTest.java
│ │ ├── AgentRuntimeTest.java
│ │ ├── AgentTeamAgentAdapterTest.java
│ │ ├── AgentTeamPersistenceTest.java
│ │ ├── AgentTeamProjectDeliveryExampleTest.java
│ │ ├── AgentTeamTaskBoardTest.java
│ │ ├── AgentTeamTest.java
│ │ ├── AgentTeamUsageTest.java
│ │ ├── AgentTraceListenerTest.java
│ │ ├── AgentTraceUsageTest.java
│ │ ├── AgentWorkflowTest.java
│ │ ├── AgentWorkflowUsageTest.java
│ │ ├── ChatModelClientTest.java
│ │ ├── CodeActAgentUsageTest.java
│ │ ├── CodeActPythonExecutorTest.java
│ │ ├── CodeActRuntimeTest.java
│ │ ├── CodeActRuntimeWithTraceTest.java
│ │ ├── DoubaoAgentTeamBestPracticeTest.java
│ │ ├── DoubaoAgentWorkflowTest.java
│ │ ├── DoubaoProjectTeamAgentTeamsTest.java
│ │ ├── FileAgentTeamStateStoreTest.java
│ │ ├── HandoffPolicyTest.java
│ │ ├── MinimaxAgentTeamTravelUsageTest.java
│ │ ├── NashornCodeExecutorTest.java
│ │ ├── ReActAgentUsageTest.java
│ │ ├── ResponsesModelClientTest.java
│ │ ├── StateGraphWorkflowTest.java
│ │ ├── SubAgentParallelFallbackTest.java
│ │ ├── SubAgentRuntimeTest.java
│ │ ├── SubAgentUsageTest.java
│ │ ├── ToolUtilExecutorRestrictionTest.java
│ │ ├── UniversalAgentUsageTest.java
│ │ ├── WeatherAgentWorkflowTest.java
│ │ └── support/
│ │ └── ZhipuAgentTestSupport.java
│ └── ai4j/
│ └── agent/
│ ├── flowgram/
│ │ └── FlowGramRuntimeServiceTest.java
│ ├── memory/
│ │ └── JdbcAgentMemoryTest.java
│ ├── model/
│ │ └── ChatModelClientTest.java
│ └── trace/
│ └── LangfuseTraceExporterTest.java
├── ai4j-bom/
│ └── pom.xml
├── ai4j-cli/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── io/
│ │ │ └── github/
│ │ │ └── lnyocly/
│ │ │ └── ai4j/
│ │ │ ├── cli/
│ │ │ │ ├── Ai4jCli.java
│ │ │ │ ├── Ai4jCliMain.java
│ │ │ │ ├── ApprovalMode.java
│ │ │ │ ├── CliProtocol.java
│ │ │ │ ├── CliUiMode.java
│ │ │ │ ├── SlashCommandController.java
│ │ │ │ ├── acp/
│ │ │ │ │ ├── AcpCodingCliAgentFactory.java
│ │ │ │ │ ├── AcpCommand.java
│ │ │ │ │ ├── AcpJsonRpcServer.java
│ │ │ │ │ ├── AcpSlashCommandSupport.java
│ │ │ │ │ └── AcpToolApprovalDecorator.java
│ │ │ │ ├── agent/
│ │ │ │ │ └── CliCodingAgentRegistry.java
│ │ │ │ ├── command/
│ │ │ │ │ ├── CodeCommand.java
│ │ │ │ │ ├── CodeCommandOptions.java
│ │ │ │ │ ├── CodeCommandOptionsParser.java
│ │ │ │ │ ├── CustomCommandRegistry.java
│ │ │ │ │ └── CustomCommandTemplate.java
│ │ │ │ ├── config/
│ │ │ │ │ └── CliWorkspaceConfig.java
│ │ │ │ ├── factory/
│ │ │ │ │ ├── CodingCliAgentFactory.java
│ │ │ │ │ ├── CodingCliTuiFactory.java
│ │ │ │ │ ├── DefaultCodingCliAgentFactory.java
│ │ │ │ │ └── DefaultCodingCliTuiFactory.java
│ │ │ │ ├── mcp/
│ │ │ │ │ ├── CliMcpConfig.java
│ │ │ │ │ ├── CliMcpConfigManager.java
│ │ │ │ │ ├── CliMcpConnectionHandle.java
│ │ │ │ │ ├── CliMcpRuntimeManager.java
│ │ │ │ │ ├── CliMcpServerDefinition.java
│ │ │ │ │ ├── CliMcpStatusSnapshot.java
│ │ │ │ │ ├── CliResolvedMcpConfig.java
│ │ │ │ │ └── CliResolvedMcpServer.java
│ │ │ │ ├── provider/
│ │ │ │ │ ├── CliProviderConfigManager.java
│ │ │ │ │ ├── CliProviderProfile.java
│ │ │ │ │ ├── CliProvidersConfig.java
│ │ │ │ │ └── CliResolvedProviderConfig.java
│ │ │ │ ├── render/
│ │ │ │ │ ├── AssistantTranscriptRenderer.java
│ │ │ │ │ ├── CliAnsi.java
│ │ │ │ │ ├── CliDisplayWidth.java
│ │ │ │ │ ├── CliThemeStyler.java
│ │ │ │ │ ├── CodexStyleBlockFormatter.java
│ │ │ │ │ ├── PatchSummaryFormatter.java
│ │ │ │ │ └── TranscriptPrinter.java
│ │ │ │ ├── runtime/
│ │ │ │ │ ├── AgentHandoffSessionEventSupport.java
│ │ │ │ │ ├── AgentTeamMessageSessionEventSupport.java
│ │ │ │ │ ├── AgentTeamSessionEventSupport.java
│ │ │ │ │ ├── CliTeamStateManager.java
│ │ │ │ │ ├── CliToolApprovalDecorator.java
│ │ │ │ │ ├── CodingCliSessionRunner.java
│ │ │ │ │ ├── CodingCliTuiSupport.java
│ │ │ │ │ ├── CodingTaskSessionEventBridge.java
│ │ │ │ │ ├── HeadlessCodingSessionRuntime.java
│ │ │ │ │ ├── HeadlessTurnObserver.java
│ │ │ │ │ └── TeamBoardRenderSupport.java
│ │ │ │ ├── session/
│ │ │ │ │ ├── CodingSessionManager.java
│ │ │ │ │ ├── CodingSessionStore.java
│ │ │ │ │ ├── DefaultCodingSessionManager.java
│ │ │ │ │ ├── FileCodingSessionStore.java
│ │ │ │ │ ├── FileSessionEventStore.java
│ │ │ │ │ ├── InMemoryCodingSessionStore.java
│ │ │ │ │ ├── InMemorySessionEventStore.java
│ │ │ │ │ ├── SessionEventStore.java
│ │ │ │ │ └── StoredCodingSession.java
│ │ │ │ └── shell/
│ │ │ │ ├── JlineCodeCommandRunner.java
│ │ │ │ ├── JlineShellContext.java
│ │ │ │ ├── JlineShellTerminalIO.java
│ │ │ │ └── WindowsConsoleKeyPoller.java
│ │ │ └── tui/
│ │ │ ├── AnsiTuiRuntime.java
│ │ │ ├── AppendOnlyTuiRuntime.java
│ │ │ ├── JlineTerminalIO.java
│ │ │ ├── StreamsTerminalIO.java
│ │ │ ├── TerminalIO.java
│ │ │ ├── TuiAnsi.java
│ │ │ ├── TuiAssistantPhase.java
│ │ │ ├── TuiAssistantToolView.java
│ │ │ ├── TuiAssistantViewModel.java
│ │ │ ├── TuiConfig.java
│ │ │ ├── TuiConfigManager.java
│ │ │ ├── TuiInteractionState.java
│ │ │ ├── TuiKeyStroke.java
│ │ │ ├── TuiKeyType.java
│ │ │ ├── TuiPaletteItem.java
│ │ │ ├── TuiPanelId.java
│ │ │ ├── TuiRenderContext.java
│ │ │ ├── TuiRenderer.java
│ │ │ ├── TuiRuntime.java
│ │ │ ├── TuiScreenModel.java
│ │ │ ├── TuiSessionView.java
│ │ │ ├── TuiTheme.java
│ │ │ ├── io/
│ │ │ │ ├── DefaultJlineTerminalIO.java
│ │ │ │ └── DefaultStreamsTerminalIO.java
│ │ │ └── runtime/
│ │ │ ├── DefaultAnsiTuiRuntime.java
│ │ │ └── DefaultAppendOnlyTuiRuntime.java
│ │ └── resources/
│ │ └── io/
│ │ └── github/
│ │ └── lnyocly/
│ │ └── ai4j/
│ │ └── tui/
│ │ └── themes/
│ │ ├── amber.json
│ │ ├── default.json
│ │ ├── github-dark.json
│ │ ├── github-light.json
│ │ ├── matrix.json
│ │ └── ocean.json
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ └── ai4j/
│ ├── cli/
│ │ ├── Ai4jCliTest.java
│ │ ├── AssistantTranscriptRendererTest.java
│ │ ├── CliDisplayWidthTest.java
│ │ ├── CliMcpConfigManagerTest.java
│ │ ├── CliMcpRuntimeManagerTest.java
│ │ ├── CliProviderConfigManagerTest.java
│ │ ├── CliThemeStylerTest.java
│ │ ├── CodeCommandOptionsParserTest.java
│ │ ├── CodeCommandTest.java
│ │ ├── CodexStyleBlockFormatterTest.java
│ │ ├── DefaultCodingCliAgentFactoryTest.java
│ │ ├── DefaultCodingSessionManagerTest.java
│ │ ├── FileCodingSessionStoreTest.java
│ │ ├── FileSessionEventStoreTest.java
│ │ ├── JlineShellTerminalIOTest.java
│ │ ├── PatchSummaryFormatterTest.java
│ │ ├── SlashCommandControllerTest.java
│ │ ├── TranscriptPrinterTest.java
│ │ ├── acp/
│ │ │ ├── AcpCommandTest.java
│ │ │ └── AcpSlashCommandSupportTest.java
│ │ ├── agent/
│ │ │ └── CliCodingAgentRegistryTest.java
│ │ └── runtime/
│ │ ├── AgentHandoffSessionEventSupportTest.java
│ │ ├── AgentTeamMessageSessionEventSupportTest.java
│ │ ├── AgentTeamSessionEventSupportTest.java
│ │ ├── CodingTaskSessionEventBridgeTest.java
│ │ └── HeadlessCodingSessionRuntimeTest.java
│ └── tui/
│ ├── AnsiTuiRuntimeTest.java
│ ├── AppendOnlyTuiRuntimeTest.java
│ ├── StreamsTerminalIOTest.java
│ ├── TuiConfigManagerTest.java
│ ├── TuiInteractionStateTest.java
│ └── TuiSessionViewTest.java
├── ai4j-coding/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── io/
│ │ └── github/
│ │ └── lnyocly/
│ │ └── ai4j/
│ │ └── coding/
│ │ ├── CodingAgent.java
│ │ ├── CodingAgentBuilder.java
│ │ ├── CodingAgentOptions.java
│ │ ├── CodingAgentRequest.java
│ │ ├── CodingAgentResult.java
│ │ ├── CodingAgents.java
│ │ ├── CodingSession.java
│ │ ├── CodingSessionCheckpoint.java
│ │ ├── CodingSessionCheckpointFormatter.java
│ │ ├── CodingSessionCompactResult.java
│ │ ├── CodingSessionScope.java
│ │ ├── CodingSessionSnapshot.java
│ │ ├── CodingSessionState.java
│ │ ├── compact/
│ │ │ ├── CodingCompactionPreparation.java
│ │ │ ├── CodingSessionCompactor.java
│ │ │ ├── CodingToolResultMicroCompactResult.java
│ │ │ └── CodingToolResultMicroCompactor.java
│ │ ├── definition/
│ │ │ ├── BuiltInCodingAgentDefinitions.java
│ │ │ ├── CodingAgentDefinition.java
│ │ │ ├── CodingAgentDefinitionRegistry.java
│ │ │ ├── CodingApprovalMode.java
│ │ │ ├── CodingIsolationMode.java
│ │ │ ├── CodingMemoryScope.java
│ │ │ ├── CodingSessionMode.java
│ │ │ ├── CompositeCodingAgentDefinitionRegistry.java
│ │ │ └── StaticCodingAgentDefinitionRegistry.java
│ │ ├── delegate/
│ │ │ ├── CodingDelegateRequest.java
│ │ │ ├── CodingDelegateResult.java
│ │ │ ├── CodingDelegateToolExecutor.java
│ │ │ └── CodingDelegateToolRegistry.java
│ │ ├── loop/
│ │ │ ├── CodingAgentLoopController.java
│ │ │ ├── CodingContinuationPrompt.java
│ │ │ ├── CodingLoopDecision.java
│ │ │ ├── CodingLoopPolicy.java
│ │ │ └── CodingStopReason.java
│ │ ├── patch/
│ │ │ ├── ApplyPatchFileChange.java
│ │ │ └── ApplyPatchResult.java
│ │ ├── policy/
│ │ │ ├── CodingToolContextPolicy.java
│ │ │ └── CodingToolPolicyResolver.java
│ │ ├── process/
│ │ │ ├── BashProcessInfo.java
│ │ │ ├── BashProcessLogChunk.java
│ │ │ ├── BashProcessStatus.java
│ │ │ ├── SessionProcessRegistry.java
│ │ │ └── StoredProcessSnapshot.java
│ │ ├── prompt/
│ │ │ └── CodingContextPromptAssembler.java
│ │ ├── runtime/
│ │ │ ├── CodingRuntime.java
│ │ │ ├── CodingRuntimeListener.java
│ │ │ └── DefaultCodingRuntime.java
│ │ ├── session/
│ │ │ ├── CodingSessionDescriptor.java
│ │ │ ├── CodingSessionLink.java
│ │ │ ├── CodingSessionLinkStore.java
│ │ │ ├── InMemoryCodingSessionLinkStore.java
│ │ │ ├── ManagedCodingSession.java
│ │ │ ├── SessionEvent.java
│ │ │ └── SessionEventType.java
│ │ ├── shell/
│ │ │ ├── LocalShellCommandExecutor.java
│ │ │ ├── ShellCommandExecutor.java
│ │ │ ├── ShellCommandRequest.java
│ │ │ ├── ShellCommandResult.java
│ │ │ └── ShellCommandSupport.java
│ │ ├── skill/
│ │ │ ├── CodingSkillDescriptor.java
│ │ │ └── CodingSkillDiscovery.java
│ │ ├── task/
│ │ │ ├── CodingTask.java
│ │ │ ├── CodingTaskManager.java
│ │ │ ├── CodingTaskProgress.java
│ │ │ ├── CodingTaskStatus.java
│ │ │ └── InMemoryCodingTaskManager.java
│ │ ├── tool/
│ │ │ ├── ApplyPatchToolExecutor.java
│ │ │ ├── BashToolExecutor.java
│ │ │ ├── CodingToolNames.java
│ │ │ ├── CodingToolRegistryFactory.java
│ │ │ ├── ReadFileToolExecutor.java
│ │ │ ├── RoutingToolExecutor.java
│ │ │ ├── ToolExecutorDecorator.java
│ │ │ └── WriteFileToolExecutor.java
│ │ └── workspace/
│ │ ├── LocalWorkspaceFileService.java
│ │ ├── WorkspaceContext.java
│ │ ├── WorkspaceEntry.java
│ │ ├── WorkspaceFileReadResult.java
│ │ ├── WorkspaceFileService.java
│ │ └── WorkspaceWriteResult.java
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ └── ai4j/
│ └── coding/
│ ├── ApplyPatchToolExecutorTest.java
│ ├── BashToolExecutorTest.java
│ ├── CodingAgentBuilderTest.java
│ ├── CodingRuntimeTest.java
│ ├── CodingSessionCheckpointFormatterTest.java
│ ├── CodingSessionTest.java
│ ├── CodingSkillSupportTest.java
│ ├── LocalShellCommandExecutorTest.java
│ ├── MinimaxCodingAgentTeamWorkspaceUsageTest.java
│ ├── ReadFileToolExecutorTest.java
│ ├── WriteFileToolExecutorTest.java
│ ├── loop/
│ │ └── CodingAgentLoopControllerTest.java
│ └── shell/
│ └── ShellCommandSupportTest.java
├── ai4j-flowgram-demo/
│ ├── README.md
│ ├── backend-18080-run.log
│ ├── backend-18080.log
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── io/
│ │ └── github/
│ │ └── lnyocly/
│ │ └── ai4j/
│ │ └── flowgram/
│ │ └── demo/
│ │ ├── FlowGramDemoApplication.java
│ │ └── FlowGramDemoMockController.java
│ └── resources/
│ └── application.yml
├── ai4j-flowgram-spring-boot-starter/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── io/
│ │ │ └── github/
│ │ │ └── lnyocly/
│ │ │ └── ai4j/
│ │ │ └── flowgram/
│ │ │ └── springboot/
│ │ │ ├── adapter/
│ │ │ │ └── FlowGramProtocolAdapter.java
│ │ │ ├── autoconfigure/
│ │ │ │ └── FlowGramAutoConfiguration.java
│ │ │ ├── config/
│ │ │ │ └── FlowGramProperties.java
│ │ │ ├── controller/
│ │ │ │ └── FlowGramTaskController.java
│ │ │ ├── dto/
│ │ │ │ ├── FlowGramErrorResponse.java
│ │ │ │ ├── FlowGramTaskCancelResponse.java
│ │ │ │ ├── FlowGramTaskReportResponse.java
│ │ │ │ ├── FlowGramTaskResultResponse.java
│ │ │ │ ├── FlowGramTaskRunRequest.java
│ │ │ │ ├── FlowGramTaskRunResponse.java
│ │ │ │ ├── FlowGramTaskValidateRequest.java
│ │ │ │ ├── FlowGramTaskValidateResponse.java
│ │ │ │ └── FlowGramTraceView.java
│ │ │ ├── exception/
│ │ │ │ ├── FlowGramAccessDeniedException.java
│ │ │ │ ├── FlowGramApiException.java
│ │ │ │ ├── FlowGramExceptionHandler.java
│ │ │ │ └── FlowGramTaskNotFoundException.java
│ │ │ ├── node/
│ │ │ │ ├── FlowGramCodeNodeExecutor.java
│ │ │ │ ├── FlowGramHttpNodeExecutor.java
│ │ │ │ ├── FlowGramKnowledgeRetrieveNodeExecutor.java
│ │ │ │ ├── FlowGramNodeValueResolver.java
│ │ │ │ ├── FlowGramToolNodeExecutor.java
│ │ │ │ └── FlowGramVariableNodeExecutor.java
│ │ │ ├── security/
│ │ │ │ ├── DefaultFlowGramAccessChecker.java
│ │ │ │ ├── DefaultFlowGramCallerResolver.java
│ │ │ │ ├── DefaultFlowGramTaskOwnershipStrategy.java
│ │ │ │ ├── FlowGramAccessChecker.java
│ │ │ │ ├── FlowGramAction.java
│ │ │ │ ├── FlowGramCaller.java
│ │ │ │ ├── FlowGramCallerResolver.java
│ │ │ │ ├── FlowGramTaskOwnership.java
│ │ │ │ └── FlowGramTaskOwnershipStrategy.java
│ │ │ └── support/
│ │ │ ├── FlowGramRuntimeFacade.java
│ │ │ ├── FlowGramRuntimeTraceCollector.java
│ │ │ ├── FlowGramStoredTask.java
│ │ │ ├── FlowGramTaskStore.java
│ │ │ ├── FlowGramTraceResponseEnricher.java
│ │ │ ├── InMemoryFlowGramTaskStore.java
│ │ │ ├── JdbcFlowGramTaskStore.java
│ │ │ └── RegistryBackedFlowGramModelClientResolver.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── spring.factories
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ └── ai4j/
│ └── flowgram/
│ └── springboot/
│ ├── FlowGramJdbcTaskStoreAutoConfigurationTest.java
│ ├── FlowGramRuntimeTraceCollectorTest.java
│ ├── FlowGramTaskControllerIntegrationTest.java
│ ├── node/
│ │ ├── FlowGramBuiltinNodeExecutorTest.java
│ │ └── FlowGramKnowledgeRetrieveNodeExecutorTest.java
│ └── support/
│ └── JdbcFlowGramTaskStoreTest.java
├── ai4j-flowgram-webapp-demo/
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── README.zh_CN.md
│ ├── index.html
│ ├── package.json
│ ├── rsbuild.config.ts
│ ├── src/
│ │ ├── app.tsx
│ │ ├── assets/
│ │ │ ├── icon-auto-layout.tsx
│ │ │ ├── icon-cancel.tsx
│ │ │ ├── icon-comment.tsx
│ │ │ ├── icon-minimap.tsx
│ │ │ ├── icon-mouse.tsx
│ │ │ ├── icon-pad.tsx
│ │ │ ├── icon-success.tsx
│ │ │ ├── icon-switch-line.tsx
│ │ │ └── icon-warning.tsx
│ │ ├── components/
│ │ │ ├── add-node/
│ │ │ │ ├── index.tsx
│ │ │ │ └── use-add-node.ts
│ │ │ ├── base-node/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── node-wrapper.tsx
│ │ │ │ ├── styles.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── comment/
│ │ │ │ ├── components/
│ │ │ │ │ ├── blank-area.tsx
│ │ │ │ │ ├── border-area.tsx
│ │ │ │ │ ├── container.tsx
│ │ │ │ │ ├── content-drag-area.tsx
│ │ │ │ │ ├── drag-area.tsx
│ │ │ │ │ ├── editor.tsx
│ │ │ │ │ ├── index.css
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── more-button.tsx
│ │ │ │ │ ├── render.tsx
│ │ │ │ │ └── resize-area.tsx
│ │ │ │ ├── constant.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── use-model.ts
│ │ │ │ │ ├── use-overflow.ts
│ │ │ │ │ ├── use-placeholder.ts
│ │ │ │ │ └── use-size.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── model.ts
│ │ │ │ └── type.ts
│ │ │ ├── group/
│ │ │ │ ├── color.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── background.tsx
│ │ │ │ │ ├── color.tsx
│ │ │ │ │ ├── header.tsx
│ │ │ │ │ ├── icon-group.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── node-render.tsx
│ │ │ │ │ ├── tips/
│ │ │ │ │ │ ├── global-store.ts
│ │ │ │ │ │ ├── icon-close.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── is-mac-os.ts
│ │ │ │ │ │ ├── style.ts
│ │ │ │ │ │ └── use-control.ts
│ │ │ │ │ ├── title.tsx
│ │ │ │ │ ├── tools.tsx
│ │ │ │ │ └── ungroup.tsx
│ │ │ │ ├── constant.ts
│ │ │ │ ├── index.css
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── line-add-button/
│ │ │ │ ├── button.tsx
│ │ │ │ ├── index.less
│ │ │ │ ├── index.tsx
│ │ │ │ └── use-visible.ts
│ │ │ ├── node-menu/
│ │ │ │ └── index.tsx
│ │ │ ├── node-panel/
│ │ │ │ ├── index.less
│ │ │ │ ├── index.tsx
│ │ │ │ ├── node-list.tsx
│ │ │ │ └── node-placeholder.tsx
│ │ │ ├── problem-panel/
│ │ │ │ ├── index.ts
│ │ │ │ ├── problem-panel.tsx
│ │ │ │ └── use-watch-validate.ts
│ │ │ ├── selector-box-popover/
│ │ │ │ └── index.tsx
│ │ │ ├── sidebar/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── node-form-panel.tsx
│ │ │ │ └── sidebar-node-renderer.tsx
│ │ │ ├── testrun/
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── use-fields.ts
│ │ │ │ │ ├── use-form-meta.ts
│ │ │ │ │ └── use-sync-default.ts
│ │ │ │ ├── json-value-editor/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── node-status-bar/
│ │ │ │ │ ├── group/
│ │ │ │ │ │ ├── index.module.less
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── header/
│ │ │ │ │ │ ├── index.module.less
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── render/
│ │ │ │ │ │ ├── index.module.less
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── viewer/
│ │ │ │ │ ├── index.module.less
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── testrun-button/
│ │ │ │ │ ├── index.module.less
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── testrun-form/
│ │ │ │ │ ├── index.module.less
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── type.ts
│ │ │ │ ├── testrun-json-input/
│ │ │ │ │ ├── index.module.less
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── testrun-panel/
│ │ │ │ │ ├── index.module.less
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── test-run-panel.tsx
│ │ │ │ └── trace-panel/
│ │ │ │ ├── index.module.less
│ │ │ │ └── index.tsx
│ │ │ └── tools/
│ │ │ ├── auto-layout.tsx
│ │ │ ├── comment.tsx
│ │ │ ├── download.tsx
│ │ │ ├── fit-view.tsx
│ │ │ ├── index.tsx
│ │ │ ├── interactive.tsx
│ │ │ ├── minimap-switch.tsx
│ │ │ ├── minimap.tsx
│ │ │ ├── mouse-pad-selector.less
│ │ │ ├── mouse-pad-selector.tsx
│ │ │ ├── readonly.tsx
│ │ │ ├── save.tsx
│ │ │ ├── styles.tsx
│ │ │ ├── switch-line.tsx
│ │ │ └── zoom-select.tsx
│ │ ├── context/
│ │ │ ├── index.ts
│ │ │ ├── node-render-context.ts
│ │ │ └── sidebar-context.ts
│ │ ├── data/
│ │ │ └── workflow-templates.ts
│ │ ├── editor.tsx
│ │ ├── form-components/
│ │ │ ├── feedback.tsx
│ │ │ ├── form-content/
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.tsx
│ │ │ ├── form-header/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── styles.tsx
│ │ │ │ ├── title-input.tsx
│ │ │ │ └── utils.tsx
│ │ │ ├── form-inputs/
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.tsx
│ │ │ ├── form-item/
│ │ │ │ ├── index.css
│ │ │ │ └── index.tsx
│ │ │ └── index.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── use-editor-props.tsx
│ │ │ ├── use-is-sidebar.ts
│ │ │ ├── use-node-render-context.ts
│ │ │ └── use-port-click.ts
│ │ ├── index.ts
│ │ ├── initial-data.ts
│ │ ├── nodes/
│ │ │ ├── block-end/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── block-start/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── break/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── code/
│ │ │ │ ├── components/
│ │ │ │ │ ├── code.tsx
│ │ │ │ │ ├── inputs.tsx
│ │ │ │ │ └── outputs.tsx
│ │ │ │ ├── form-meta.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── types.tsx
│ │ │ ├── comment/
│ │ │ │ └── index.tsx
│ │ │ ├── condition/
│ │ │ │ ├── condition-inputs/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── styles.tsx
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── constants.ts
│ │ │ ├── continue/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── default-form-meta.tsx
│ │ │ ├── end/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── group/
│ │ │ │ └── index.tsx
│ │ │ ├── http/
│ │ │ │ ├── components/
│ │ │ │ │ ├── api.tsx
│ │ │ │ │ ├── body.tsx
│ │ │ │ │ ├── headers.tsx
│ │ │ │ │ ├── params.tsx
│ │ │ │ │ └── timeout.tsx
│ │ │ │ ├── form-meta.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── types.tsx
│ │ │ ├── index.ts
│ │ │ ├── knowledge/
│ │ │ │ └── index.tsx
│ │ │ ├── llm/
│ │ │ │ └── index.ts
│ │ │ ├── loop/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── start/
│ │ │ │ ├── form-meta.tsx
│ │ │ │ └── index.ts
│ │ │ ├── tool/
│ │ │ │ └── index.tsx
│ │ │ └── variable/
│ │ │ ├── form-meta.tsx
│ │ │ ├── index.tsx
│ │ │ ├── output-schema.ts
│ │ │ └── types.tsx
│ │ ├── plugins/
│ │ │ ├── context-menu-plugin/
│ │ │ │ ├── context-menu-layer.tsx
│ │ │ │ ├── context-menu-plugin.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── panel-manager-plugin/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── hooks.ts
│ │ │ │ └── index.tsx
│ │ │ ├── runtime-plugin/
│ │ │ │ ├── client/
│ │ │ │ │ ├── base-client.ts
│ │ │ │ │ ├── browser-client/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── server-client/
│ │ │ │ │ ├── constant.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── type.ts
│ │ │ │ ├── create-runtime-plugin.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-service/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── trace.ts
│ │ │ │ └── type.ts
│ │ │ └── variable-panel-plugin/
│ │ │ ├── components/
│ │ │ │ ├── full-variable-list.tsx
│ │ │ │ ├── global-variable-editor.tsx
│ │ │ │ ├── index.module.less
│ │ │ │ └── variable-panel.tsx
│ │ │ ├── index.ts
│ │ │ ├── variable-panel-layer.tsx
│ │ │ └── variable-panel-plugin.ts
│ │ ├── services/
│ │ │ ├── custom-service.ts
│ │ │ ├── index.ts
│ │ │ └── validate-service.ts
│ │ ├── shortcuts/
│ │ │ ├── collapse/
│ │ │ │ └── index.ts
│ │ │ ├── constants.ts
│ │ │ ├── copy/
│ │ │ │ └── index.ts
│ │ │ ├── delete/
│ │ │ │ └── index.ts
│ │ │ ├── expand/
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── paste/
│ │ │ │ ├── index.ts
│ │ │ │ ├── traverse.ts
│ │ │ │ └── unique-workflow.ts
│ │ │ ├── select-all/
│ │ │ │ └── index.ts
│ │ │ ├── shortcuts.ts
│ │ │ ├── type.ts
│ │ │ ├── zoom-in/
│ │ │ │ └── index.ts
│ │ │ └── zoom-out/
│ │ │ └── index.ts
│ │ ├── styles/
│ │ │ └── index.css
│ │ ├── type.d.ts
│ │ ├── typings/
│ │ │ ├── index.ts
│ │ │ ├── json-schema.ts
│ │ │ └── node.ts
│ │ ├── utils/
│ │ │ ├── backend-workflow.ts
│ │ │ ├── can-contain-node.ts
│ │ │ ├── index.ts
│ │ │ ├── on-drag-line-end.ts
│ │ │ └── toggle-loop-expanded.ts
│ │ └── workbench/
│ │ ├── runtime-hooks.ts
│ │ ├── workbench-shell.tsx
│ │ ├── workbench-sidebar.tsx
│ │ └── workbench-toolbar.tsx
│ └── tsconfig.json
├── ai4j-spring-boot-starter/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── io/
│ │ │ └── github/
│ │ │ └── lnyocly/
│ │ │ └── ai4j/
│ │ │ ├── AgentFlowProperties.java
│ │ │ ├── AgentFlowRegistry.java
│ │ │ ├── AiConfigAutoConfiguration.java
│ │ │ ├── AiConfigProperties.java
│ │ │ ├── AiPlatformProperties.java
│ │ │ ├── BaichuanConfigProperties.java
│ │ │ ├── DashScopeConfigProperties.java
│ │ │ ├── DeepSeekConfigProperties.java
│ │ │ ├── DoubaoConfigProperties.java
│ │ │ ├── HunyuanConfigProperties.java
│ │ │ ├── JinaConfigProperties.java
│ │ │ ├── LingyiConfigProperties.java
│ │ │ ├── MilvusConfigProperties.java
│ │ │ ├── MinimaxConfigProperties.java
│ │ │ ├── MoonshotConfigProperties.java
│ │ │ ├── OkHttpConfigProperties.java
│ │ │ ├── OllamaConfigProperties.java
│ │ │ ├── OpenAiConfigProperties.java
│ │ │ ├── PgVectorConfigProperties.java
│ │ │ ├── PineconeConfigProperties.java
│ │ │ ├── QdrantConfigProperties.java
│ │ │ ├── SearXNGConfigProperties.java
│ │ │ └── ZhipuConfigProperties.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── spring.factories
│ └── test/
│ └── java/
│ └── io/
│ └── github/
│ └── lnyocly/
│ └── ai4j/
│ └── AgentFlowAutoConfigurationTest.java
├── docs-site/
│ ├── .gitignore
│ ├── README.md
│ ├── docusaurus.config.ts
│ ├── i18n/
│ │ └── zh-Hans/
│ │ ├── code.json
│ │ ├── docusaurus-plugin-content-docs/
│ │ │ ├── current/
│ │ │ │ ├── agent/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── agent-teams.md
│ │ │ │ │ ├── codeact-custom-sandbox.md
│ │ │ │ │ ├── codeact-runtime.md
│ │ │ │ │ ├── coding-agent-cli.md
│ │ │ │ │ ├── coding-agent-command-reference.md
│ │ │ │ │ ├── custom-agent-development.md
│ │ │ │ │ ├── memory-management.md
│ │ │ │ │ ├── model-client-selection.md
│ │ │ │ │ ├── multi-provider-profiles.md
│ │ │ │ │ ├── overview.md
│ │ │ │ │ ├── provider-config-examples.md
│ │ │ │ │ ├── reference-core-classes.md
│ │ │ │ │ ├── runtime-implementations.md
│ │ │ │ │ ├── subagent-handoff-policy.md
│ │ │ │ │ ├── system-prompt-vs-instructions.md
│ │ │ │ │ ├── trace-observability.md
│ │ │ │ │ ├── weather-workflow-cookbook.md
│ │ │ │ │ └── workflow-stategraph.md
│ │ │ │ ├── ai-basics/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── _category_.json
│ │ │ │ │ │ ├── multimodal.md
│ │ │ │ │ │ ├── non-stream.md
│ │ │ │ │ │ ├── stream.md
│ │ │ │ │ │ └── tool-calling.md
│ │ │ │ │ ├── enhancements/
│ │ │ │ │ │ ├── _category_.json
│ │ │ │ │ │ ├── pinecone-rag-workflow.md
│ │ │ │ │ │ ├── searxng-enhancement.md
│ │ │ │ │ │ └── spi-http-stack.md
│ │ │ │ │ ├── overview.md
│ │ │ │ │ ├── platform-adaptation.md
│ │ │ │ │ ├── responses/
│ │ │ │ │ │ ├── _category_.json
│ │ │ │ │ │ ├── chat-vs-responses.md
│ │ │ │ │ │ ├── non-stream.md
│ │ │ │ │ │ └── stream-events.md
│ │ │ │ │ └── services/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── audio.md
│ │ │ │ │ ├── embedding.md
│ │ │ │ │ ├── image-generation.md
│ │ │ │ │ └── realtime.md
│ │ │ │ ├── core-sdk/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── agentflow-protocol-mapping.md
│ │ │ │ │ ├── agentflow.md
│ │ │ │ │ ├── audio.md
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ ├── multimodal.md
│ │ │ │ │ │ ├── non-stream.md
│ │ │ │ │ │ ├── stream.md
│ │ │ │ │ │ └── tool-calling.md
│ │ │ │ │ ├── embedding.md
│ │ │ │ │ ├── image-generation.md
│ │ │ │ │ ├── overview.md
│ │ │ │ │ ├── pinecone-rag-workflow.md
│ │ │ │ │ ├── platform-service-matrix.md
│ │ │ │ │ ├── realtime.md
│ │ │ │ │ ├── responses/
│ │ │ │ │ │ ├── chat-vs-responses.md
│ │ │ │ │ │ ├── non-stream.md
│ │ │ │ │ │ └── stream-events.md
│ │ │ │ │ ├── searxng-enhancement.md
│ │ │ │ │ └── spi-http-stack.md
│ │ │ │ ├── deploy/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ └── cloudflare-pages.md
│ │ │ │ ├── getting-started/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── chat-and-responses-guide.md
│ │ │ │ │ ├── coding-agent-cli-quickstart.md
│ │ │ │ │ ├── installation.md
│ │ │ │ │ ├── multimodal-and-function-call.md
│ │ │ │ │ ├── platforms-and-service-matrix.md
│ │ │ │ │ ├── quickstart-ollama.md
│ │ │ │ │ ├── quickstart-openai-jdk8.md
│ │ │ │ │ ├── quickstart-springboot.md
│ │ │ │ │ └── troubleshooting.md
│ │ │ │ ├── guides/
│ │ │ │ │ ├── _category_.json
│ │ │ │ │ ├── blog-migration-map.md
│ │ │ │ │ ├── deepseek-stream-search-rag.md
│ │ │ │ │ ├── pinecone-vector-workflow.md
│ │ │ │ │ ├── rag-legal-assistant.md
│ │ │ │ │ ├── searxng-web-search.md
│ │ │ │ │ └── spi-dispatcher-connectionpool.md
│ │ │ │ ├── intro.md
│ │ │ │ └── mcp/
│ │ │ │ ├── _category_.json
│ │ │ │ ├── build-your-mcp-server.md
│ │ │ │ ├── client-integration.md
│ │ │ │ ├── gateway-management.md
│ │ │ │ ├── mcp-agent-end-to-end.md
│ │ │ │ ├── mysql-dynamic-datasource.md
│ │ │ │ ├── overview.md
│ │ │ │ ├── third-party-mcp-integration.md
│ │ │ │ ├── tool-exposure-semantics.md
│ │ │ │ └── transport-types.md
│ │ │ └── current.json
│ │ └── docusaurus-theme-classic/
│ │ ├── footer.json
│ │ └── navbar.json
│ ├── package.json
│ ├── scripts/
│ │ └── generate_agent_teams_api_docs.py
│ ├── sidebars.ts
│ ├── src/
│ │ ├── components/
│ │ │ └── HomepageFeatures/
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ │ ├── css/
│ │ │ └── custom.css
│ │ ├── pages/
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ └── theme/
│ │ └── NotFound/
│ │ └── Content/
│ │ └── index.tsx
│ ├── static/
│ │ ├── .nojekyll
│ │ ├── install.ps1
│ │ └── install.sh
│ └── tsconfig.json
└── pom.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .github/workflows/docs-build.yml
================================================
name: docs-build
on:
pull_request:
paths:
- 'docs-site/**'
- '.github/workflows/docs-build.yml'
push:
branches: [main, dev]
paths:
- 'docs-site/**'
- '.github/workflows/docs-build.yml'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs-site
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: docs-site/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build docs
run: npm run build
================================================
FILE: .github/workflows/docs-pages.yml
================================================
name: docs-pages
on:
push:
branches: [main]
paths:
- 'docs-site/**'
- '.github/workflows/docs-pages.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: docs-pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs-site
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: docs-site/package-lock.json
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: npm ci
- name: Build docs
env:
GITHUB_PAGES: 'true'
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs-site/build
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .gitignore
================================================
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Local tooling ###
.claude/
AGENTS.md
node_modules/
dist/
.rsbuild/
.turbo/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
### Scratch / temp ###
*.class
.tmp*/
.tmp*
tmp/
tmp-*/
tmp-*.png
tmp-*.txt
tmp-*.log
**/.flattened-pom.xml
### Mac OS ###
.DS_Store
/.idea/
/ai4j/.gitignore
/ai4j/src/main/resources/新建文本文档 (2).txt
/ai4j-spring-boot-stater/.gitignore
.ai4j/
.docs/
docs/
APIResponse.md
AGENT.md
ai4j-release-package.log
javadoc-ai4j.log
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://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.
================================================
FILE: README-EN.md
================================================
# ai4j
A Java AI Agentic development toolkit for JDK 8+, combining foundational AI capabilities with higher-level agent development capabilities.
It covers multi-provider model access, unified I/O, Tool Calling, MCP, RAG, unified `VectorStore`, ChatMemory, agent runtime, coding agent, CLI / TUI / ACP, FlowGram integration, and integration with published AgentFlow endpoints such as Dify, Coze, and n8n, helping Java applications grow from basic model integration to more complete agentic application development.
This repository has evolved into a multi-module SDK. In addition to the core `ai4j` module, it now provides `ai4j-agent`, `ai4j-coding`, `ai4j-cli`, `ai4j-spring-boot-starter`, `ai4j-flowgram-spring-boot-starter`, and `ai4j-bom`. If you only need the basic LLM integration layer, start with `ai4j`. If you need agent runtime, coding agent, CLI / ACP, Spring Boot, or FlowGram integration, add the corresponding modules.
## Positioning Compared with Common Java AI Options
| Option | Java baseline | Application style | Primary focus |
| --- | --- | --- | --- |
| `ai4j` | `JDK 8+` | Plain Java / Spring | Unified model access, Tool / MCP / RAG, agent runtime, coding agent, CLI / TUI / ACP |
| `Spring AI` | `Java 17+` | `Spring Boot 3.x` | Spring-native AI integration, model access, Tool Calling, MCP, and RAG |
| `Spring AI Alibaba` | `Java 17+` | `Spring Boot 3.x` | Spring and Alibaba Cloud AI ecosystem integration |
| `LangChain4j` | `Java 17+` | Plain Java / Spring / Quarkus and more | General Java abstractions for LLM, agent, and RAG integration, plus AI Services |
## Supported platforms
+ OpenAi
+ Jina (Rerank / Jina-compatible Rerank)
+ Zhipu
+ DeepSeek
+ Moonshot
+ Tencent Hunyuan
+ Lingyi AI
+ Ollama
+ MiniMax
+ Baichuan
## Supported services
+ Chat Completions(streaming and non-streaming)
+ Responses
+ Embedding
+ Rerank
+ Audio
+ Image
+ Realtime
## Supported AgentFlow / hosted workflow platforms
+ Dify (chat / workflow)
+ Coze (chat / workflow)
+ n8n (webhook workflow)
## Features
+ Supports Spring and ordinary Java applications. Supports applications above Java 8.
+ Multi-platform and multi-service.
+ Provides `AgentFlow` support for integrating published Agent / Workflow endpoints from Dify, Coze, and n8n.
+ Provides `ai4j-agent` as the general agent runtime, with ReAct, subagents, agent teams, memory, tracing, and tool loop support.
+ Built-in Coding Agent CLI / TUI with interactive repository sessions, provider profiles, workspace model override, and session/process management.
+ Provides `ai4j-coding` as the coding agent runtime, with workspace-aware tools, outer loop, checkpoint compaction, subagent, and team collaboration support.
+ Provides `ai4j-flowgram-spring-boot-starter` for integrating FlowGram workflows and trace in Spring Boot applications.
+ Provides `ai4j-bom` for version alignment across multiple ai4j modules.
+ Unified input and output.
+ Unified error handling.
+ Supports streaming output. Supports streaming output of function call parameters.
+ Easily use Tool Calls.
+ Supports simultaneous calls of multiple functions (Zhipu does not support this).
+ Supports stream_options, and directly obtains statistical token usage through streaming output.
+ Supports RAG. Built-in vector database support: Pinecone.
+ Uses Tika to read files.
+ Token statistics`TikTokensUtil.java`
## Tutorial documents
+ [Quick access to Spring Boot, access to streaming and non-streaming and function calls.](http://t.csdnimg.cn/iuIAW)
+ [Quick access to open source large models such as qwen2.5 and llama3.1 on the Ollama platform in Java.](https://blog.csdn.net/qq_35650513/article/details/142408092?spm=1001.2014.3001.5501)
+ [Build a legal AI assistant in Java and quickly implement RAG applications.](https://blog.csdn.net/qq_35650513/article/details/142568177?fromshare=blogdetail&sharetype=blogdetail&sharerId=142568177&sharerefer=PC&sharesource=qq_35650513&sharefrom=from_link)
## Coding Agent CLI / TUI
AI4J now includes `ai4j-cli`, which can be used directly as a local coding agent. Current capabilities include:
+ one-shot and persistent sessions
+ CLI and TUI interaction modes
+ provider profile persistence
+ workspace-level model override
+ subagent and agent team collaboration
+ session persistence, resume, fork, history, tree, events, replay
+ team board, team messages, and team resume for collaboration visibility
+ process management and buffered logs
### Install
```bash
curl -fsSL https://lnyo-cly.github.io/ai4j/install.sh | sh
```
```powershell
irm https://lnyo-cly.github.io/ai4j/install.ps1 | iex
```
The installer downloads `ai4j-cli` from Maven Central and creates the `ai4j` command. Java 8+ must already be installed on the machine.
### one-shot example
```powershell
ai4j code `
--provider openai `
--protocol responses `
--model gpt-5-mini `
--prompt "Read README and summarize the project structure"
```
### interactive CLI example
```powershell
ai4j code `
--provider zhipu `
--protocol chat `
--model glm-4.7 `
--base-url https://open.bigmodel.cn/api/coding/paas/v4 `
--workspace .
```
### TUI example
```powershell
ai4j tui `
--provider zhipu `
--protocol chat `
--model glm-4.7 `
--base-url https://open.bigmodel.cn/api/coding/paas/v4 `
--workspace .
```
### ACP example
```powershell
ai4j acp `
--provider openai `
--protocol responses `
--model gpt-5-mini `
--workspace .
```
### Build from source (optional)
```powershell
mvn -pl ai4j-cli -am -DskipTests package
```
Artifact:
```text
ai4j-cli/target/ai4j-cli--jar-with-dependencies.jar
```
If you want to run the locally built artifact directly:
```powershell
java -jar .\ai4j-cli\target\ai4j-cli--jar-with-dependencies.jar code --help
```
### Current protocol rules
The CLI currently exposes only two protocol families:
+ `chat`
+ `responses`
If `--protocol` is omitted, the CLI resolves a default locally from provider/baseUrl:
+ `openai` + official OpenAI host -> `responses`
+ `openai` + custom compatible `baseUrl` -> `chat`
+ `doubao` / `dashscope` -> `responses`
+ other providers -> `chat`
Notes:
+ `auto` is no longer exposed to users
+ legacy `auto` values in existing config files are normalized to explicit protocols on load
### provider profile locations
+ global config: `~/.ai4j/providers.json`
+ workspace config: `/.ai4j/workspace.json`
Recommended workflow:
+ keep reusable long-term runtime profiles in the global config
+ let each workspace reference one `activeProfile`
+ use workspace `modelOverride` for temporary model switching
### Common commands
+ `/providers`
+ `/provider`
+ `/provider use `
+ `/provider save `
+ `/provider add --provider [--protocol ] [--model ] [--base-url ] [--api-key ]`
+ `/provider edit [--provider ] [--protocol ] [--model |--clear-model] [--base-url |--clear-base-url] [--api-key |--clear-api-key]`
+ `/provider default `
+ `/provider remove `
+ `/model`
+ `/model `
+ `/model reset`
+ `/stream [on|off]`
+ `/processes`
+ `/process status|follow|logs|write|stop ...`
+ `/resume ` / `/load ` / `/fork ...`
### Documentation entry points
+ [Coding Agent CLI Quickstart](docs-site/docs/getting-started/coding-agent-cli-quickstart.md)
+ [Coding Agent CLI and TUI](docs-site/docs/agent/coding-agent-cli.md)
+ [Multi-Provider Profiles](docs-site/docs/agent/multi-provider-profiles.md)
+ [Coding Agent Command Reference](docs-site/docs/agent/coding-agent-command-reference.md)
+ [Provider Configuration Examples](docs-site/docs/agent/provider-config-examples.md)
## Other support
+ [[Low-cost transit platform] Low-cost ApiKey - Limited-time special offer 0.7:1 - Supports the latest o1 model.](https://api.trovebox.online/)
# Quick start
## Import
### Module selection
+ Use `ai4j` for the core LLM / Tool Call / MCP / RAG capabilities
+ Use `ai4j-agent` for the general agent runtime
+ Use `ai4j-coding` for coding agent, workspace tools, and outer loop
+ Use `ai4j-cli` for the local CLI / TUI / ACP host
+ Use `ai4j-spring-boot-starter` for Spring Boot auto-configuration
+ Use `ai4j-flowgram-spring-boot-starter` for FlowGram workflow integration
+ Use `ai4j-bom` when you want version alignment across multiple modules
### Gradle
```groovy
implementation platform("io.github.lnyo-cly:ai4j-bom:${project.version}")
implementation "io.github.lnyo-cly:ai4j"
implementation "io.github.lnyo-cly:ai4j-agent"
```
```groovy
implementation group: 'io.github.lnyo-cly', name: 'ai4j', version: '${project.version}'
```
```groovy
implementation group: 'io.github.lnyo-cly', name: 'ai4j-spring-boot-starter', version: '${project.version}'
```
### Maven
```xml
io.github.lnyo-cly
ai4j-bom
${project.version}
pom
import
```
```xml
io.github.lnyo-cly
ai4j-agent
io.github.lnyo-cly
ai4j-coding
```
```xml
io.github.lnyo-cly
ai4j
${project.version}
```
```xml
io.github.lnyo-cly
ai4j-spring-boot-starter
${project.version}
```
## Obtain AI service instance
### Obtaining without Spring
```java
public void test_init(){
OpenAiConfig openAiConfig = new OpenAiConfig();
Configuration configuration = new Configuration();
configuration.setOpenAiConfig(openAiConfig);
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new ErrorInterceptor())
.connectTimeout(300, TimeUnit.SECONDS)
.writeTimeout(300, TimeUnit.SECONDS)
.readTimeout(300, TimeUnit.SECONDS)
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1",10809)))
.build();
configuration.setOkHttpClient(okHttpClient);
AiService aiService = new AiService(configuration);
embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
chatService = aiService.getChatService(PlatformType.getPlatform("OPENAI"));
}
```
### Obtaining with Spring
```yml
# Domestic access usually requires a proxy by default.
ai:
openai:
api-key: "api-key"
okhttp:
proxy-port: 10809
proxy-url: "127.0.0.1"
zhipu:
api-key: "xxx"
#other...
```
```java
// Inject Ai service
@Autowired
private AiService aiService;
// Obtain the required service instance
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// ......
```
## Chat service
### Synchronous request call
```java
public void test_chat() throws Exception {
// Obtain chat service instance
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// Build request parameters
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("鲁迅为什么打周树人"))
.build();
// Send dialogue request
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
### Streaming call
```java
public void test_chat_stream() throws Exception {
// Obtain chat service instance
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// Construct request parameters
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("查询北京明天的天气"))
.functions("queryWeather")
.build();
// Construct listener
SseListener sseListener = new SseListener() {
@Override
protected void send() {
System.out.println(this.getCurrStr());
}
};
// Display function parameters. Default is not to display.
sseListener.setShowToolArgs(true);
// Send SSE request
chatService.chatCompletionStream(chatCompletion, sseListener);
System.out.println(sseListener.getOutput());
}
```
### Image recognition
```java
public void test_chat_image() throws Exception {
// Obtain chat service instance
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// Build request parameters
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("图片中有什么东西", "https://cn.bing.com/images/search?view=detailV2&ccid=r0OnuYkv&id=9A07DE578F6ED50DB59DFEA5C675AC71845A6FC9&thid=OIP.r0OnuYkvsbqBrYk3kUT53AHaKX&mediaurl=https%3a%2f%2fimg.zcool.cn%2fcommunity%2f0104c15cd45b49a80121416816f1ec.jpg%401280w_1l_2o_100sh.jpg&exph=1792&expw=1280&q=%e5%b0%8f%e7%8c%ab%e5%9b%be%e7%89%87&simid=607987191780608963&FORM=IRPRST&ck=12127C1696CF374CB9D0F09AE99AFE69&selectedIndex=2&itb=0&qpvt=%e5%b0%8f%e7%8c%ab%e5%9b%be%e7%89%87"))
.build();
// Send dialogue request
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
### Function call
```java
public void test_chat_tool_call() throws Exception {
// Obtain chat service instance
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// Build request parameters
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("今天北京天气怎么样"))
.functions("queryWeather")
.build();
// Send dialogue request
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
#### Define function
```java
@FunctionCall(name = "queryWeather", description = "查询目标地点的天气预报")
public class QueryWeatherFunction implements Function {
@Data
@FunctionRequest
public static class Request{
@FunctionParameter(description = "需要查询天气的目标位置, 可以是城市中文名、城市拼音/英文名、省市名称组合、IP 地址、经纬度")
private String location;
@FunctionParameter(description = "需要查询未来天气的天数, 最多15日")
private int days = 15;
@FunctionParameter(description = "预报的天气类型,daily表示预报多天天气、hourly表示预测当天24天气、now为当前天气实况")
private Type type;
}
public enum Type{
daily,
hourly,
now
}
@Override
public String apply(Request request) {
final String key = "";
String url = String.format("https://api.seniverse.com/v3/weather/%s.json?key=%s&location=%s&days=%d",
request.type.name(),
key,
request.location,
request.days);
OkHttpClient client = new OkHttpClient();
okhttp3.Request http = new okhttp3.Request.Builder()
.url(url)
.get()
.build();
try (Response response = client.newCall(http).execute()) {
if (response.isSuccessful()) {
// 解析响应体
return response.body() != null ? response.body().string() : "";
} else {
return "获取天气失败 当前天气未知";
}
} catch (Exception e) {
// 处理异常
e.printStackTrace();
return "获取天气失败 当前天气未知";
}
}
}
```
## Embedding service
```java
public void test_embed() throws Exception {
// Obtain embedding service instance
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// Build request parameters
Embedding embeddingReq = Embedding.builder().input("1+1").build();
// Send embedding request
EmbeddingResponse embeddingResp = embeddingService.embedding(embeddingReq);
System.out.println(embeddingResp);
}
```
## RAG
### Configure vector database
```yml
ai:
vector:
pinecone:
url: ""
key: ""
```
### Obtain instance
```java
@Autowired
private PineconeService pineconeService;
```
### Insert into vector database
```java
public void test_insert_vector_store() throws Exception {
// Obtain embedding service instance
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// Read file content using Tika
String fileContent = TikaUtil.parseFile(new File("D:\\data\\test\\test.txt"));
// Split text content
RecursiveCharacterTextSplitter recursiveCharacterTextSplitter = new RecursiveCharacterTextSplitter(1000, 200);
List contentList = recursiveCharacterTextSplitter.splitText(fileContent);
// Convert to vector
Embedding build = Embedding.builder()
.input(contentList)
.model("text-embedding-3-small")
.build();
EmbeddingResponse embedding = embeddingService.embedding(build);
List> vectors = embedding.getData().stream().map(EmbeddingObject::getEmbedding).collect(Collectors.toList());
VertorDataEntity vertorDataEntity = new VertorDataEntity();
vertorDataEntity.setVector(vectors);
vertorDataEntity.setContent(contentList);
// Vector storage
Integer count = pineconeService.insert(vertorDataEntity, "userId");
}
```
### Query from vector database
```java
public void test_query_vector_store() throws Exception {
// // Obtain embedding service instance
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// Build the question to be queried and convert it to a vector
Embedding build = Embedding.builder()
.input("question")
.model("text-embedding-3-small")
.build();
EmbeddingResponse embedding = embeddingService.embedding(build);
List question = embedding.getData().get(0).getEmbedding();
// Build the query object for the vector database
PineconeQuery pineconeQueryReq = PineconeQuery.builder()
.namespace("userId")
.vector(question)
.build();
String result = pineconeService.query(pineconeQueryReq, " ");
// Carry the result and have a conversation with the chat service.
// ......
}
```
### Delete data from vector database
```java
public void test_delete_vector_store() throws Exception {
// Build parameters
PineconeDelete pineconeDelete = PineconeDelete.builder()
.deleteAll(true)
.namespace("userId")
.build();
// Delete
Boolean res = pineconeService.delete(pineconeDelete);
}
```
# Contribute to ai4j
You are welcome to provide suggestions, report issues, or contribute code to ai4j. You can contribute to ai4j in the following ways:
## Issue feedback
Please use the GitHub Issue page to report issues. Describe as specifically as possible how to reproduce your issue, including detailed information such as the operating system, Java version, and any relevant log traces.
## PR
1. Fork this repository and create your branch.
2. Write your code and test it.
3. Ensure that your code conforms to the existing style.
4. Write clear log information when submitting. For small changes, a single line of information is sufficient, but for larger changes, there should be a detailed description.
5. Complete the pull request form and ensure that changes are made on the `dev` branch and link to the issue that your PR addresses.
# Support
If you find this project helpful to you, please give it a star⭐。
================================================
FILE: README.md
================================================
# ai4j
一款面向 JDK8+ 的 Java AI Agentic 开发套件,既提供统一的大模型调用与常用 AI 基座能力,也提供更完善的智能体式 Agent 开发能力。
覆盖多平台模型接入、统一输入输出、Tool Call、MCP、RAG、统一 `VectorStore`、ChatMemory、Agent Runtime、Coding Agent、CLI / TUI / ACP、FlowGram 集成,以及 Dify / Coze / n8n 等已发布 AgentFlow 端点接入能力,帮助 Java 应用从基础模型接入扩展到更完整的 agentic 应用开发。
当前仓库已经演进为多模块 SDK,除核心 `ai4j` 外,还提供 `ai4j-agent`、`ai4j-coding`、`ai4j-cli`、`ai4j-spring-boot-starter`、`ai4j-flowgram-spring-boot-starter`、`ai4j-bom`。如果只需要基础大模型调用,优先引入 `ai4j`;如果需要 Agent、Coding Agent、CLI / ACP、Spring Boot 或 FlowGram 集成,再按模块引入对应能力。
## 适用场景与常见方案对比
| 方案 | Java 基线 | 应用形态 | 能力侧重点 |
| --- | --- | --- | --- |
| `ai4j` | `JDK8+` | 普通 Java / Spring | 统一大模型接入、Tool / MCP / RAG、Agent Runtime、Coding Agent、CLI / TUI / ACP |
| `Spring AI` | `Java 17+` | `Spring Boot 3.x` | Spring 原生 AI 集成、模型访问、Tool Calling、MCP、RAG |
| `Spring AI Alibaba` | `Java 17+` | `Spring Boot 3.x` | Spring 与阿里云 AI 生态整合 |
| `LangChain4j` | `Java 17+` | 普通 Java / Spring / Quarkus 等 | 通用 Java LLM / Agent / RAG 抽象、AI Services、多框架集成 |
## 支持的平台
+ OpenAi(包含与OpenAi请求格式相同/兼容的平台)
+ Jina(Rerank / Jina-compatible Rerank)
+ Zhipu(智谱)
+ DeepSeek(深度求索)
+ Moonshot(月之暗面)
+ Hunyuan(腾讯混元)
+ Lingyi(零一万物)
+ Ollama
+ MiniMax
+ Baichuan
## 支持的服务
+ Chat Completions(流式与非流式)
+ Responses
+ Embedding
+ Rerank
+ Audio
+ Image
+ Realtime
## 已适配的 AgentFlow / 工作流平台
+ Dify(Chat / Workflow)
+ Coze(Chat / Workflow)
+ n8n(Webhook Workflow)
## 特性
+ 支持MCP服务,内置MCP网关,支持建立动态MCP数据源。
+ 支持Spring以及普通Java应用、支持Java 8以上的应用
+ 多平台、多服务
+ 提供 `AgentFlow` 能力,可直接接入 Dify、Coze、n8n 等已发布 Agent / Workflow 端点
+ 提供 `ai4j-agent` 通用 Agent 运行时,支持 ReAct、subagent、agent teams、memory、trace 与 tool loop
+ 内置 Coding Agent CLI / TUI,支持本地代码仓交互式会话、provider profile、workspace model override、session/process 管理
+ 提供 `ai4j-coding` Coding Agent 运行时,支持 workspace tools、outer loop、checkpoint compaction、subagent 与 team 协作
+ 提供 `ai4j-flowgram-spring-boot-starter`,便于在 Spring Boot 中接入 FlowGram 工作流与 trace
+ 提供 `ai4j-bom`,便于多模块项目统一版本管理
+ 统一的输入输出
+ 统一的错误处理
+ 支持SPI机制,可自定义Dispatcher和ConnectPool
+ 支持服务增强,例如增加websearch服务
+ 支持流式输出。支持函数调用参数流式输出.
+ 简洁的多模态调用方式,例如vision识图
+ 轻松使用Tool Calls
+ 支持多个函数同时调用(智谱不支持)
+ 支持stream_options,流式输出直接获取统计token usage
+ 内置 `ChatMemory`,支持基础多轮会话上下文维护,可同时适配 Chat / Responses
+ 支持RAG,内置统一 `VectorStore` 抽象,当前支持: Pinecone、Qdrant、pgvector、Milvus
+ 内置 `IngestionPipeline`,统一串联 `DocumentLoader -> Chunker -> MetadataEnricher -> Embedding -> VectorStore.upsert`
+ 内置 `DenseRetriever`、`Bm25Retriever`、`HybridRetriever`,可按语义检索、关键词检索、混合检索方式组合知识库召回
+ `HybridRetriever` 支持 `RrfFusionStrategy`、`RsfFusionStrategy`、`DbsfFusionStrategy`,默认使用 RRF;融合排序与 `Reranker` 语义精排解耦
+ 支持统一 `IRerankService`,当前可接 Jina / Jina-compatible、Ollama、Doubao(方舟知识库重排);可通过 `ModelReranker` 无缝接入 RAG 精排
+ RAG 运行时可直接拿到 `rank/retrieverSource/retrievalScore/fusionScore/rerankScore/scoreDetails/trace`,并可通过 `RagEvaluator` 计算 `Precision@K/Recall@K/F1@K/MRR/NDCG`
+ 使用Tika读取文件
+ Token统计`TikTokensUtil.java`
## 官方文档站
+ 在线文档站:`https://lnyo-cly.github.io/ai4j/`
+ 文档站源码位于 `docs-site/`
+ 适合直接使用者的入口:`docs-site/docs/coding-agent/`
+ 适合 SDK 接入的入口:`docs-site/docs/getting-started/` 与 `docs-site/docs/ai-basics/`
+ 适合协议与扩展集成的入口:`docs-site/docs/mcp/`、`docs-site/docs/agent/`
推荐阅读顺序:
+ `docs-site/docs/intro.md`
+ `docs-site/docs/getting-started/installation.md`
+ `docs-site/docs/coding-agent/overview.md`
+ `docs-site/docs/ai-basics/overview.md`
+ `docs-site/docs/mcp/overview.md`
基础会话上下文新增入口:
+ `docs-site/docs/ai-basics/chat/chat-memory.md`
+ `docs-site/docs/ai-basics/services/rerank.md`
+ `docs-site/docs/ai-basics/rag/ingestion-pipeline.md`
本地运行文档站:
```powershell
cd .\docs-site
npm install
npm run start
```
```powershell
cd .\docs-site
npm run build
```
## 更新日志
+ [2026-03-28] 修复 Coding Agent ACP 流式场景下纯空白 chunk 被 runtime 过滤的问题;ACP 保持透传原始 delta,不做 chunk 聚合;补充 CLI/文档中的流式语义说明
+ [2026-03-26] 新增 Coding Agent CLI / TUI 文档与能力说明,覆盖交互式会话、provider profile、workspace model override、命令参考与配置样例
+ [2025-08-19] 修复传递有验证参数的sse-url时,key丢失问题
+ [2025-08-08] OpenAi: max_tokens字段现已废弃,推荐使用max_completion_tokens(GPT-5已经不支持max_tokens字段)
+ [2025-08-08] 支持MCP协议,支持STDIO,SSE,Streamable HTTP; 支持MCP Server与MCP Client; 支持MCP网关; 支持自定义MCP数据源; 支持MCP自动重连
+ [2025-06-23] 修复ollama的流式错误;修复ollama函数调用的错误;修复moonshot请求时错误;修复ollama embedding错误;修复思考无内容;修复日志冲突;新增自定义异常方法。
+ [2025-02-28] 新增对Ollama平台的embedding接口的支持。
+ [2025-02-17] 新增对DeepSeek平台推理模型的适配。
+ [2025-02-12] 为Ollama平台添加Authorization
+ [2025-02-11] 实现自定义的Jackson序列化,解决OpenAi已经无法通过Json String来直接实现多模态接口的问题。
+ [2024-12-12] 使用装饰器模式增强Chat服务,支持SearXNG网络搜索增强,无需模型支持内置搜索以及function_call。
+ [2024-10-17] 支持SPI机制,可自定义Dispatcher和ConnectPool。新增百川Baichuan平台Chat接口支持。
+ [2024-10-16] 增加MiniMax平台Chat接口对接
+ [2024-10-15] 增加realtime服务
+ [2024-10-12] 修复早期遗忘的小bug; 修复错误拦截器导致的音频字节流异常错误问题; 增加OpenAi Audio服务。
+ [2024-10-10] 增强对SSE输出的获取,新加入`currData`属性,记录当前消息的整个对象。而原先的`currStr`为当前消息的content内容,保留不变。
+ [2024-09-26] 修复有关Pinecone向量数据库的一些问题。发布0.6.3版本
+ [2024-09-20] 增加对Ollama平台的支持,并修复一些bug。发布0.6.2版本
+ [2024-09-19] 增加错误处理链,统一处理为openai错误类型; 修复部分情况下URL拼接问题,修复拦截器中response重复调用而导致的关闭问题。发布0.5.3版本
+ [2024-09-12] 修复上个问题OpenAi参数导致错误的遗漏,发布0.5.2版本
+ [2024-09-12] 修复SpringBoot 2.6以下导致OkHttp变为3.14版本的报错问题;修复OpenAi参数`parallel_tool_calls`在tools为null时的异常问题。发布0.5.1版本。
+ [2024-09-09] 新增零一万物大模型支持、发布0.5.0版本。
+ [2024-09-02] 新增腾讯混元Hunyuan平台支持(注意:所需apiKey 属于SecretId与SecretKey的拼接,格式为 {SecretId}.{SecretKey}),发布0.4.0版本。
+ [2024-08-30] 新增对Moonshot(Kimi)平台的支持,增加`OkHttpUtil.java`实现忽略SSL证书的校验。
+ [2024-08-29] 新增对DeepSeek平台的支持、新增stream_options可以直接统计usage、新增错误拦截器`ErrorInterceptor.java`、发布0.3.0版本。
+ [2024-08-29] 修改SseListener以兼容智谱函数调用。
+ [2024-08-28] 添加token统计、添加智谱AI的Chat服务、优化函数调用可以支持多轮多函数。
+ [2024-08-17] 增强SseListener监听器功能。发布0.2.0版本。
## 教程文档
+ [快速接入SpringBoot、接入流式与非流式以及函数调用](http://t.csdnimg.cn/iuIAW)
+ [Java快速接入qwen2.5、llama3.1等Ollama平台开源大模型](https://blog.csdn.net/qq_35650513/article/details/142408092?spm=1001.2014.3001.5501)
+ [Java搭建法律AI助手,快速实现RAG应用](https://blog.csdn.net/qq_35650513/article/details/142568177?fromshare=blogdetail&sharetype=blogdetail&sharerId=142568177&sharerefer=PC&sharesource=qq_35650513&sharefrom=from_link)
+ [大模型不支持联网搜索?为Deepseek、Qwen、llama等本地模型添加网络搜索](https://blog.csdn.net/qq_35650513/article/details/144572824)
+ [java快速接入mcp以及结合mysql动态管理](https://blog.csdn.net/qq_35650513/article/details/150532784?fromshare=blogdetail&sharetype=blogdetail&sharerId=150532784&sharerefer=PC&sharesource=qq_35650513&sharefrom=from_link)
## Coding Agent CLI / TUI
AI4J 目前已经内置 `ai4j-cli`,可以直接作为本地 coding agent 使用,支持:
+ one-shot 与持续会话
+ CLI / TUI 两种交互模式
+ provider profile 持久化
+ workspace 级 model override
+ subagent 与 agent teams 协作
+ session 持久化、resume、fork、history、tree、events、replay
+ team board、team messages、team resume 等协作观测能力
+ process 管理与日志查看
### 安装
```bash
curl -fsSL https://lnyo-cly.github.io/ai4j/install.sh | sh
```
```powershell
irm https://lnyo-cly.github.io/ai4j/install.ps1 | iex
```
安装脚本会从 Maven Central 下载 `ai4j-cli` 并生成 `ai4j` 命令,前提是本机已经安装 Java 8+。
### one-shot 示例
```powershell
ai4j code `
--provider openai `
--protocol responses `
--model gpt-5-mini `
--prompt "Read README and summarize the project structure"
```
### 交互式 CLI 示例
```powershell
ai4j code `
--provider zhipu `
--protocol chat `
--model glm-4.7 `
--base-url https://open.bigmodel.cn/api/coding/paas/v4 `
--workspace .
```
### TUI 示例
```powershell
ai4j tui `
--provider zhipu `
--protocol chat `
--model glm-4.7 `
--base-url https://open.bigmodel.cn/api/coding/paas/v4 `
--workspace .
```
### ACP 示例
```powershell
ai4j acp `
--provider openai `
--protocol responses `
--model gpt-5-mini `
--workspace .
```
### 源码构建(可选)
```powershell
mvn -pl ai4j-cli -am -DskipTests package
```
产物示例:
```text
ai4j-cli/target/ai4j-cli--jar-with-dependencies.jar
```
如果你需要直接运行本地构建产物:
```powershell
java -jar .\ai4j-cli\target\ai4j-cli--jar-with-dependencies.jar code --help
```
### 当前协议规则
当前 CLI 对用户只暴露两种协议:
+ `chat`
+ `responses`
如果省略 `--protocol`,会按 provider/baseUrl 在本地推导默认值:
+ `openai` + 官方 OpenAI host -> `responses`
+ `openai` + 自定义兼容 `baseUrl` -> `chat`
+ `doubao` / `dashscope` -> `responses`
+ 其他 provider -> `chat`
注意:
+ 不再对用户暴露 `auto`
+ 旧配置中的 `auto` 会在读取时自动归一化为显式协议
### provider profile 配置位置
+ 全局配置:`~/.ai4j/providers.json`
+ 工作区配置:`/.ai4j/workspace.json`
推荐工作流:
+ 全局保存长期可复用 profile
+ workspace 只引用当前 activeProfile
+ 临时切模型时使用 workspace 的 `modelOverride`
`workspace.json` 也可以显式挂载额外 skill 目录:
```json
{
"activeProfile": "openai-main",
"modelOverride": "gpt-5-mini",
"enabledMcpServers": ["fetch"],
"skillDirectories": [
".ai4j/skills",
"C:/skills/team",
"../shared-skills"
]
}
```
skill 发现规则:
+ 默认扫描 `/.ai4j/skills`
+ 默认扫描 `~/.ai4j/skills`
+ `skillDirectories` 中的相对路径按 workspace 根目录解析
+ 进入 CLI 后可用 `/skills` 查看当前发现到的 skill
+ 可用 `/skills ` 查看某个 skill 的路径、来源、描述和扫描 roots,不打印 `SKILL.md` 正文
### `/stream`、`Esc` 与状态提示
当前 `/stream` 的语义是“当前 CLI 会话里的模型请求是否启用 `stream`”,不是单纯的 transcript 渲染开关:
+ 作用域是当前 CLI 会话
+ `/stream on|off` 会切换请求级 `stream=true|false`,并立即重建当前 session runtime
+ `on` 时 provider 响应按增量到达,assistant 文本也按增量呈现
+ `off` 时等待完整响应后再输出整理后的完成块
+ 流式 event 粒度由上游 provider/SSE 决定,不保证“一个 event = 一个 token”
+ 如果通过 ACP/IDE 接入,宿主应按收到的 chunk 顺序渲染,并保留换行与空白
当前交互壳层里:
+ `Esc` 在活跃 turn 中断当前任务;空闲时关闭 palette 或清空输入
+ 状态栏会显示 `Thinking`、`Connecting`、`Responding`、`Working`、`Retrying`
+ 一段时间没有新进展会升级为 `Waiting`
+ 更久没有新进展会显示 `Stalled`,并提示 `press Esc to interrupt`
### 常用命令
+ `/providers`
+ `/provider`
+ `/provider use `
+ `/provider save `
+ `/provider add --provider [--protocol ] [--model ] [--base-url ] [--api-key ]`
+ `/provider edit [--provider ] [--protocol ] [--model |--clear-model] [--base-url |--clear-base-url] [--api-key |--clear-api-key]`
+ `/provider default `
+ `/provider remove `
+ `/model`
+ `/model `
+ `/model reset`
+ `/skills`
+ `/skills `
+ `/stream [on|off]`
+ `/processes`
+ `/process status|follow|logs|write|stop ...`
+ `/resume ` / `/load ` / `/fork ...`
### 文档入口
+ [Coding Agent 总览](docs-site/docs/coding-agent/overview.md)
+ [Coding Agent 快速开始](docs-site/docs/coding-agent/quickstart.md)
+ [CLI / TUI 使用指南](docs-site/docs/coding-agent/cli-and-tui.md)
+ [会话、流式与进程](docs-site/docs/coding-agent/session-runtime.md)
+ [配置体系](docs-site/docs/coding-agent/configuration.md)
+ [Tools 与审批机制](docs-site/docs/coding-agent/tools-and-approvals.md)
+ [Skills 使用与组织](docs-site/docs/coding-agent/skills.md)
+ [MCP 对接](docs-site/docs/coding-agent/mcp-integration.md)
+ [ACP 集成](docs-site/docs/coding-agent/acp-integration.md)
+ [TUI 定制与主题](docs-site/docs/coding-agent/tui-customization.md)
+ [命令参考](docs-site/docs/coding-agent/command-reference.md)
## 其它支持
+ [[低价中转平台] 低价ApiKey—限时特惠 ](https://api.trovebox.online/)
+ [[在线平台] 每日白嫖额度-所有模型均可使用 ](https://chat.trovebox.online/)
# 快速开始
## 导入
### 模块选型
+ 只需要基础 LLM / Tool Call / MCP / RAG 能力:引入 `ai4j`
+ 需要通用 Agent 运行时:引入 `ai4j-agent`
+ 需要 Coding Agent、workspace tools、outer loop:引入 `ai4j-coding`
+ 需要本地 CLI / TUI / ACP 宿主:引入 `ai4j-cli`
+ 需要 Spring Boot 自动配置:引入 `ai4j-spring-boot-starter`
+ 需要 FlowGram 工作流集成:引入 `ai4j-flowgram-spring-boot-starter`
+ 同时引入多个模块:建议额外引入 `ai4j-bom`
### Gradle
```groovy
implementation platform("io.github.lnyo-cly:ai4j-bom:${project.version}")
implementation "io.github.lnyo-cly:ai4j"
implementation "io.github.lnyo-cly:ai4j-agent"
```
```groovy
implementation group: 'io.github.lnyo-cly', name: 'ai4j', version: '${project.version}'
```
```groovy
implementation group: 'io.github.lnyo-cly', name: 'ai4j-spring-boot-starter', version: '${project.version}'
```
### Maven
```xml
io.github.lnyo-cly
ai4j-bom
${project.version}
pom
import
```
```xml
io.github.lnyo-cly
ai4j-agent
io.github.lnyo-cly
ai4j-coding
```
```xml
io.github.lnyo-cly
ai4j
${project.version}
```
```xml
io.github.lnyo-cly
ai4j-spring-boot-starter
${project.version}
```
## 获取AI服务实例
### 非Spring获取
```java
public void test_init(){
OpenAiConfig openAiConfig = new OpenAiConfig();
Configuration configuration = new Configuration();
configuration.setOpenAiConfig(openAiConfig);
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new ErrorInterceptor())
.connectTimeout(300, TimeUnit.SECONDS)
.writeTimeout(300, TimeUnit.SECONDS)
.readTimeout(300, TimeUnit.SECONDS)
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1",10809)))
.build();
configuration.setOkHttpClient(okHttpClient);
AiService aiService = new AiService(configuration);
embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
chatService = aiService.getChatService(PlatformType.getPlatform("OPENAI"));
}
```
### Spring获取
```yml
# 国内访问默认需要代理
ai:
openai:
api-key: "api-key"
okhttp:
proxy-port: 10809
proxy-url: "127.0.0.1"
zhipu:
api-key: "xxx"
#other...
```
```java
// 注入Ai服务
@Autowired
private AiService aiService;
// 获取需要的服务实例
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// ......
```
## Chat服务
### 同步请求调用
```java
public void test_chat() throws Exception {
// 获取chat服务实例
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// 构建请求参数
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("鲁迅为什么打周树人"))
.build();
// 发送对话请求
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
### 流式调用
```java
public void test_chat_stream() throws Exception {
// 获取chat服务实例
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// 构造请求参数
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("查询北京明天的天气"))
.functions("queryWeather")
.build();
// 构造监听器
SseListener sseListener = new SseListener() {
@Override
protected void send() {
System.out.println(this.getCurrStr());
}
};
// 显示函数参数,默认不显示
sseListener.setShowToolArgs(true);
// 发送SSE请求
chatService.chatCompletionStream(chatCompletion, sseListener);
System.out.println(sseListener.getOutput());
}
```
### 图片识别
```java
public void test_chat_image() throws Exception {
// 获取chat服务实例
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// 构建请求参数
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("图片中有什么东西", "https://cn.bing.com/images/search?view=detailV2&ccid=r0OnuYkv&id=9A07DE578F6ED50DB59DFEA5C675AC71845A6FC9&thid=OIP.r0OnuYkvsbqBrYk3kUT53AHaKX&mediaurl=https%3a%2f%2fimg.zcool.cn%2fcommunity%2f0104c15cd45b49a80121416816f1ec.jpg%401280w_1l_2o_100sh.jpg&exph=1792&expw=1280&q=%e5%b0%8f%e7%8c%ab%e5%9b%be%e7%89%87&simid=607987191780608963&FORM=IRPRST&ck=12127C1696CF374CB9D0F09AE99AFE69&selectedIndex=2&itb=0&qpvt=%e5%b0%8f%e7%8c%ab%e5%9b%be%e7%89%87"))
.build();
// 发送对话请求
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
### 函数调用
```java
public void test_chat_tool_call() throws Exception {
// 获取chat服务实例
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
// 构建请求参数
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("今天北京天气怎么样"))
.functions("queryWeather")
.build();
// 发送对话请求
ChatCompletionResponse response = chatService.chatCompletion(chatCompletion);
System.out.println(response);
}
```
### 内置 ChatMemory
如果你只是做基础多轮对话,不想自己每轮维护完整上下文,可以直接使用 `ChatMemory`:
```java
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
ChatMemory memory = new InMemoryChatMemory(new MessageWindowChatMemoryPolicy(12));
memory.addSystem("你是一个简洁的 Java 助手");
memory.addUser("请用三点介绍 AI4J");
ChatCompletion request = ChatCompletion.builder()
.model("gpt-4o-mini")
.messages(memory.toChatMessages())
.build();
ChatCompletionResponse response = chatService.chatCompletion(request);
String answer = response.getChoices().get(0).getMessage().getContent().getText();
memory.addAssistant(answer);
```
同一份 `memory` 也可以直接给 `Responses`:
```java
IResponsesService responsesService = aiService.getResponsesService(PlatformType.DOUBAO);
ResponseRequest request = ResponseRequest.builder()
.model("doubao-seed-1-8-251228")
.input(memory.toResponsesInput())
.build();
```
#### 定义函数
```java
@FunctionCall(name = "queryWeather", description = "查询目标地点的天气预报")
public class QueryWeatherFunction implements Function {
@Data
@FunctionRequest
public static class Request{
@FunctionParameter(description = "需要查询天气的目标位置, 可以是城市中文名、城市拼音/英文名、省市名称组合、IP 地址、经纬度")
private String location;
@FunctionParameter(description = "需要查询未来天气的天数, 最多15日")
private int days = 15;
@FunctionParameter(description = "预报的天气类型,daily表示预报多天天气、hourly表示预测当天24天气、now为当前天气实况")
private Type type;
}
public enum Type{
daily,
hourly,
now
}
@Override
public String apply(Request request) {
final String key = "";
String url = String.format("https://api.seniverse.com/v3/weather/%s.json?key=%s&location=%s&days=%d",
request.type.name(),
key,
request.location,
request.days);
OkHttpClient client = new OkHttpClient();
okhttp3.Request http = new okhttp3.Request.Builder()
.url(url)
.get()
.build();
try (Response response = client.newCall(http).execute()) {
if (response.isSuccessful()) {
// 解析响应体
return response.body() != null ? response.body().string() : "";
} else {
return "获取天气失败 当前天气未知";
}
} catch (Exception e) {
// 处理异常
e.printStackTrace();
return "获取天气失败 当前天气未知";
}
}
}
```
## Embedding服务
```java
public void test_embed() throws Exception {
// 获取embedding服务实例
IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);
// 构建请求参数
Embedding embeddingReq = Embedding.builder().input("1+1").build();
// 发送embedding请求
EmbeddingResponse embeddingResp = embeddingService.embedding(embeddingReq);
System.out.println(embeddingResp);
}
```
## Rerank服务
### 直接调用统一重排服务
```java
IRerankService rerankService = aiService.getRerankService(PlatformType.JINA);
RerankRequest request = RerankRequest.builder()
.model("jina-reranker-v2-base-multilingual")
.query("哪段最适合回答 Java 8 为什么仍然常见")
.documents(Arrays.asList(
RerankDocument.builder().id("doc-1").text("Java 8 仍是很多传统系统的默认运行时").build(),
RerankDocument.builder().id("doc-2").text("AI4J 提供统一 Chat、Responses 和 RAG 接口").build(),
RerankDocument.builder().id("doc-3").text("历史中间件和升级成本让很多企业延后 JDK 升级").build()
))
.topN(2)
.build();
RerankResponse response = rerankService.rerank(request);
System.out.println(response.getResults());
```
### 作为 RAG 精排器接入
```java
Reranker reranker = aiService.getModelReranker(
PlatformType.JINA,
"jina-reranker-v2-base-multilingual",
5,
"优先保留制度原文、版本说明和编号明确的片段"
);
```
## RAG
### 推荐:使用统一 IngestionPipeline 入库
```java
VectorStore vectorStore = aiService.getQdrantVectorStore();
IngestionPipeline ingestionPipeline = aiService.getIngestionPipeline(
PlatformType.OPENAI,
vectorStore
);
IngestionResult ingestResult = ingestionPipeline.ingest(IngestionRequest.builder()
.dataset("kb_docs")
.embeddingModel("text-embedding-3-small")
.document(RagDocument.builder()
.sourceName("员工手册")
.sourcePath("/docs/employee-handbook.md")
.tenant("acme")
.biz("hr")
.version("2026.03")
.build())
.source(IngestionSource.text("第一章 假期政策。第二章 报销政策。"))
.build());
System.out.println(ingestResult.getUpsertedCount());
```
如果你已经走 Pinecone,也可以直接:
```java
IngestionPipeline ingestionPipeline = aiService.getPineconeIngestionPipeline(PlatformType.OPENAI);
```
推荐主线是:
1. `IngestionPipeline` 负责文档入库
2. `VectorStore` 负责底层向量存储
3. `DenseRetriever / HybridRetriever / ModelReranker / RagService` 负责查询阶段
完整说明见:
+ `docs-site/docs/ai-basics/rag/ingestion-pipeline.md`
+ `docs-site/docs/ai-basics/rag/overview.md`
### 配置向量数据库
```yml
ai:
vector:
pinecone:
host: ""
key: ""
```
### 推荐:Pinecone 也走统一 `VectorStore + IngestionPipeline`
```java
VectorStore vectorStore = aiService.getPineconeVectorStore();
IngestionPipeline ingestionPipeline = aiService.getPineconeIngestionPipeline(PlatformType.OPENAI);
IngestionResult ingestResult = ingestionPipeline.ingest(IngestionRequest.builder()
.dataset("tenant_a_hr_v202603")
.embeddingModel("text-embedding-3-small")
.document(RagDocument.builder()
.sourceName("员工手册")
.sourcePath("/docs/employee-handbook.pdf")
.tenant("tenant_a")
.biz("hr")
.version("2026.03")
.build())
.source(IngestionSource.file(new File("D:/data/employee-handbook.pdf")))
.build());
System.out.println("upserted=" + ingestResult.getUpsertedCount());
```
### 查询阶段:直接走统一 `RagService`
```java
RagService ragService = aiService.getRagService(
PlatformType.OPENAI,
vectorStore
);
RagQuery ragQuery = RagQuery.builder()
.query("年假如何计算")
.dataset("tenant_a_hr_v202603")
.embeddingModel("text-embedding-3-small")
.topK(5)
.build();
RagResult ragResult = ragService.search(ragQuery);
System.out.println(ragResult.getContext());
System.out.println(ragResult.getCitations());
```
### 如果需要更高精度,再接 Rerank
```java
Reranker reranker = aiService.getModelReranker(
PlatformType.JINA,
"jina-reranker-v2-base-multilingual",
5,
"优先制度原文、章节标题和编号明确的片段"
);
RagService ragService = new DefaultRagService(
new DenseRetriever(
aiService.getEmbeddingService(PlatformType.OPENAI),
vectorStore
),
reranker,
new DefaultRagContextAssembler()
);
```
### 什么时候还需要直接用已废弃的 `PineconeService`(Deprecated)
`PineconeService` 目前在文档层已视为 Deprecated。只有在你明确需要 Pinecone 特有的底层控制时,才建议继续直接用:
+ namespace 级底层操作
+ 兼容旧项目里已经写死的 `PineconeQuery / PineconeDelete`
+ 你就是在做 Pinecone 专用封装,而不是面向统一 RAG 抽象开发
## 内置联网
### SearXNG
#### 配置
```java
// 非spring应用
SearXNGConfig searXNGConfig = new SearXNGConfig();
searXNGConfig.setUrl("http://127.0.0.1:8080/search");
Configuration configuration = new Configuration();
configuration.setSearXNGConfig(searXNGConfig);
```
```YML
# spring应用
ai:
websearch:
searxng:
url: http://127.0.0.1:8080/search
```
#### 使用
```java
// ...
webEnhance = aiService.webSearchEnhance(chatService);
// ...
@Test
public void test_chatCompletions_common_websearch_enhance() throws Exception {
ChatCompletion chatCompletion = ChatCompletion.builder()
.model("qwen2.5:7b")
.message(ChatMessage.withUser("鸡你太美是什么梗"))
.build();
System.out.println("请求参数");
System.out.println(chatCompletion);
ChatCompletionResponse chatCompletionResponse = webEnhance.chatCompletion(chatCompletion);
System.out.println("请求成功");
System.out.println(chatCompletionResponse);
}
```
# 为AI4J提供贡献
欢迎您对AI4J提出建议、报告问题或贡献代码。您可以按照以下的方式为AI4J提供贡献:
## 问题反馈
请使用GitHub Issue页面报告问题。尽可能具体地说明如何重现您的问题,包括操作系统、Java版本和任何相关日志跟踪等详细信息。
## PR
1. Fork 本仓库并创建您的分支(建议命名:feature/功能名、fix/问题名 或 docs/文档优化)。
2. 编写代码或修改内容(如更新文档),并完成测试(确保功能正常或文档无误)。
3. 确保您的代码符合现有的样式。
4. 提交时编写清晰的日志信息。对于小的改动,单行信息就可以了,但较大的改动应该有详细的描述。
5. 完成拉取请求表单,确保在`dev`分支进行改动,链接到您的 PR 解决的问题。
# 支持
如果您觉得这个项目对您有帮助,请点一个star⭐。
# Buy Me a Coffee
您的支持是我更新的最大的动力。

# 贡献者
# ⭐️ Star History
================================================
FILE: ai4j/pom.xml
================================================
4.0.0
io.github.lnyo-cly
ai4j
jar
2.3.0
ai4j
ai4j 核心 Java SDK,提供统一大模型接入、Tool Call、RAG 与 MCP 能力。 Core Java SDK for unified LLM access, tool calling, RAG, and MCP integration.
8
8
UTF-8
true
20.3.4
${graalvm.version}
15.6
The Apache License, Version 2.0
https://www.apache.org/licenses/LICENSE-2.0.txt
GitHub
https://github.com/LnYo-Cly/ai4j/issues
https://github.com/LnYo-Cly/ai4j
LnYo-Cly
LnYo-Cly
lnyocly@gmail.com
https://github.com/LnYo-Cly/ai4j
+8
https://github.com/LnYo-Cly/ai4j
scm:git:https://github.com/LnYo-Cly/ai4j.git
scm:git:https://github.com/LnYo-Cly/ai4j.git
org.slf4j
slf4j-simple
1.7.30
org.apache.tika
tika-core
2.9.2
org.apache.tika
tika-parsers-standard-package
2.8.0
com.knuddels
jtokkit
1.1.0
org.projectlombok
lombok
1.18.30
org.slf4j
slf4j-api
1.7.30
org.reflections
reflections
0.10.2
junit
junit
4.13.2
test
com.h2database
h2
2.2.224
test
com.squareup.okhttp3
mockwebserver
4.12.0
test
com.alibaba.fastjson2
fastjson2
2.0.43
com.fasterxml.jackson.core
jackson-annotations
2.16.0
com.fasterxml.jackson.core
jackson-databind
2.14.2
com.squareup.okhttp3
okhttp
4.12.0
com.squareup.okhttp3
okhttp-sse
4.12.0
com.squareup.okhttp3
logging-interceptor
4.12.0
com.google.guava
guava
33.0.0-jre
org.apache.commons
commons-lang3
3.12.0
com.auth0
java-jwt
4.4.0
cn.hutool
hutool-core
5.8.38
com.alibaba
dashscope-sdk-java
compile
2.19.0
org.jetbrains
annotations
13.0
org.graalvm.sdk
graal-sdk
${graalsdk.version}
${project.name}-${project.version}
org.apache.maven.plugins
maven-surefire-plugin
2.12.4
${skipTests}
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
1.8
1.8
UTF8
-parameters
true
org.projectlombok
lombok
1.18.30
nashorn-runtime
[15,)
org.openjdk.nashorn
nashorn-core
${nashorn.version}
runtime
graalpy-runtime
[17,)
24.1.2
org.graalvm.python
python-community
${graalsdk.version}
pom
runtime
release
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
jar-no-fork
org.apache.maven.plugins
maven-javadoc-plugin
3.6.3
attach-javadocs
jar
none
false
Author
a
Author:
Description
a
Description:
Date
a
Date:
org.apache.maven.plugins
maven-gpg-plugin
1.6
D:\Develop\DevelopEnv\GnuPG\bin\gpg.exe
cly
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
LnYo-Cly
true
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/AgentFlow.java
================================================
package io.github.lnyocly.ai4j.agentflow;
import io.github.lnyocly.ai4j.agentflow.chat.AgentFlowChatService;
import io.github.lnyocly.ai4j.agentflow.chat.CozeAgentFlowChatService;
import io.github.lnyocly.ai4j.agentflow.chat.DifyAgentFlowChatService;
import io.github.lnyocly.ai4j.agentflow.workflow.AgentFlowWorkflowService;
import io.github.lnyocly.ai4j.agentflow.workflow.CozeAgentFlowWorkflowService;
import io.github.lnyocly.ai4j.agentflow.workflow.DifyAgentFlowWorkflowService;
import io.github.lnyocly.ai4j.agentflow.workflow.N8nAgentFlowWorkflowService;
import io.github.lnyocly.ai4j.service.Configuration;
public class AgentFlow {
private final Configuration configuration;
private final AgentFlowConfig config;
public AgentFlow(Configuration configuration, AgentFlowConfig config) {
if (configuration == null) {
throw new IllegalArgumentException("configuration is required");
}
if (config == null) {
throw new IllegalArgumentException("agentFlowConfig is required");
}
this.configuration = configuration;
this.config = config;
}
public Configuration getConfiguration() {
return configuration;
}
public AgentFlowConfig getConfig() {
return config;
}
public AgentFlowChatService chat() {
if (config.getType() == AgentFlowType.DIFY) {
return new DifyAgentFlowChatService(configuration, config);
}
if (config.getType() == AgentFlowType.COZE) {
return new CozeAgentFlowChatService(configuration, config);
}
throw new IllegalArgumentException("Chat is not supported for agent flow type: " + config.getType());
}
public AgentFlowWorkflowService workflow() {
if (config.getType() == AgentFlowType.DIFY) {
return new DifyAgentFlowWorkflowService(configuration, config);
}
if (config.getType() == AgentFlowType.COZE) {
return new CozeAgentFlowWorkflowService(configuration, config);
}
if (config.getType() == AgentFlowType.N8N) {
return new N8nAgentFlowWorkflowService(configuration, config);
}
throw new IllegalArgumentException("Workflow is not supported for agent flow type: " + config.getType());
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/AgentFlowConfig.java
================================================
package io.github.lnyocly.ai4j.agentflow;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceListener;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowConfig {
@NonNull
private AgentFlowType type;
private String baseUrl;
private String webhookUrl;
private String apiKey;
private String botId;
private String workflowId;
private String appId;
private String userId;
private String conversationId;
@Builder.Default
private Long pollIntervalMillis = 1_000L;
@Builder.Default
private Long pollTimeoutMillis = 60_000L;
@Builder.Default
private Map headers = Collections.emptyMap();
@Builder.Default
private List traceListeners = Collections.emptyList();
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/AgentFlowException.java
================================================
package io.github.lnyocly.ai4j.agentflow;
public class AgentFlowException extends RuntimeException {
public AgentFlowException(String message) {
super(message);
}
public AgentFlowException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/AgentFlowType.java
================================================
package io.github.lnyocly.ai4j.agentflow;
public enum AgentFlowType {
DIFY,
COZE,
N8N
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/AgentFlowUsage.java
================================================
package io.github.lnyocly.ai4j.agentflow;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowUsage {
private Integer inputTokens;
private Integer outputTokens;
private Integer totalTokens;
private Object raw;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/AgentFlowChatEvent.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowChatEvent {
private String type;
private String contentDelta;
private String conversationId;
private String messageId;
private String taskId;
private boolean done;
private AgentFlowUsage usage;
private Object raw;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/AgentFlowChatListener.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
public interface AgentFlowChatListener {
void onEvent(AgentFlowChatEvent event);
default void onOpen() {
}
default void onError(Throwable throwable) {
}
default void onComplete(AgentFlowChatResponse response) {
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/AgentFlowChatRequest.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import java.util.Collections;
import java.util.Map;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowChatRequest {
@NonNull
private String prompt;
@Builder.Default
private Map inputs = Collections.emptyMap();
private String userId;
private String conversationId;
@Builder.Default
private Map metadata = Collections.emptyMap();
@Builder.Default
private Map extraBody = Collections.emptyMap();
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/AgentFlowChatResponse.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowChatResponse {
private String content;
private String conversationId;
private String messageId;
private String taskId;
private AgentFlowUsage usage;
private Object raw;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/AgentFlowChatService.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
public interface AgentFlowChatService {
AgentFlowChatResponse chat(AgentFlowChatRequest request) throws Exception;
void chatStream(AgentFlowChatRequest request, AgentFlowChatListener listener) throws Exception;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/CozeAgentFlowChatService.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import io.github.lnyocly.ai4j.agentflow.AgentFlowConfig;
import io.github.lnyocly.ai4j.agentflow.AgentFlowException;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import io.github.lnyocly.ai4j.agentflow.support.AgentFlowSupport;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceContext;
import io.github.lnyocly.ai4j.service.Configuration;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class CozeAgentFlowChatService extends AgentFlowSupport implements AgentFlowChatService {
public CozeAgentFlowChatService(Configuration configuration, AgentFlowConfig agentFlowConfig) {
super(configuration, agentFlowConfig);
}
@Override
public AgentFlowChatResponse chat(AgentFlowChatRequest request) throws Exception {
AgentFlowTraceContext traceContext = startTrace("chat", false, request);
try {
JSONObject createResponse = executeObject(buildCreateRequest(request, false));
assertCozeSuccess(createResponse);
JSONObject createData = createResponse.getJSONObject("data");
String chatId = createData == null ? null : createData.getString("id");
String conversationId = firstNonBlank(
createData == null ? null : createData.getString("conversation_id"),
defaultConversationId(request.getConversationId())
);
if (isBlank(chatId)) {
throw new AgentFlowException("Coze chat id is missing");
}
JSONObject chatData = pollChat(conversationId, chatId);
String status = chatData.getString("status");
if (!"completed".equals(status)) {
throw new AgentFlowException("Coze chat finished with status: " + status);
}
JSONObject messageResponse = executeObject(buildMessageListRequest(conversationId, chatId));
assertCozeSuccess(messageResponse);
JSONArray messages = messageResponse.getJSONArray("data");
StringBuilder content = new StringBuilder();
String messageId = null;
if (messages != null) {
for (int i = messages.size() - 1; i >= 0; i--) {
JSONObject message = messages.getJSONObject(i);
if (message == null) {
continue;
}
if (!"assistant".equals(message.getString("role"))) {
continue;
}
String messageContent = message.getString("content");
if (!isBlank(messageContent)) {
if (content.length() > 0) {
content.insert(0, "\n");
}
content.insert(0, messageContent);
if (messageId == null) {
messageId = message.getString("id");
}
}
}
}
Map raw = new LinkedHashMap();
raw.put("chat", chatData);
raw.put("messages", messages);
AgentFlowChatResponse chatResponse = AgentFlowChatResponse.builder()
.content(content.toString())
.conversationId(conversationId)
.messageId(messageId)
.taskId(chatId)
.usage(usageFromCoze(chatData.getJSONObject("usage")))
.raw(raw)
.build();
traceComplete(traceContext, chatResponse);
return chatResponse;
} catch (Exception ex) {
traceError(traceContext, ex);
throw ex;
}
}
@Override
public void chatStream(AgentFlowChatRequest request, final AgentFlowChatListener listener) throws Exception {
if (listener == null) {
throw new IllegalArgumentException("listener is required");
}
final AgentFlowTraceContext traceContext = startTrace("chat", true, request);
Request httpRequest = buildCreateRequest(request, true);
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference failure = new AtomicReference();
final AtomicReference chatIdRef = new AtomicReference();
final AtomicReference conversationIdRef = new AtomicReference();
final AtomicReference messageIdRef = new AtomicReference();
final AtomicReference usageRef = new AtomicReference();
final AtomicReference completionRef = new AtomicReference();
final StringBuilder content = new StringBuilder();
final AtomicBoolean closed = new AtomicBoolean(false);
final AtomicBoolean sawDelta = new AtomicBoolean(false);
eventSourceFactory.newEventSource(httpRequest, new EventSourceListener() {
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
listener.onOpen();
}
@Override
public void onEvent(@NotNull EventSource eventSource,
@Nullable String id,
@Nullable String type,
@NotNull String data) {
try {
String eventType = type;
JSONObject payload = parseObjectOrNull(data);
if ("done".equals(eventType)) {
AgentFlowChatEvent event = AgentFlowChatEvent.builder()
.type(eventType)
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(chatIdRef.get())
.done(true)
.usage(usageRef.get())
.raw(data)
.build();
listener.onEvent(event);
traceEvent(traceContext, event);
AgentFlowChatResponse responsePayload = AgentFlowChatResponse.builder()
.content(content.toString())
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(chatIdRef.get())
.usage(usageRef.get())
.raw(data)
.build();
completionRef.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
closed.set(true);
eventSource.cancel();
latch.countDown();
return;
}
if ("error".equals(eventType)) {
throw new AgentFlowException("Coze stream error: " + data);
}
String delta = null;
if (eventType != null && eventType.startsWith("conversation.chat.")) {
conversationIdRef.set(firstNonBlank(
payload == null ? null : payload.getString("conversation_id"),
conversationIdRef.get()
));
chatIdRef.set(firstNonBlank(
payload == null ? null : payload.getString("id"),
chatIdRef.get()
));
AgentFlowUsage usage = payload == null ? null : usageFromCoze(payload.getJSONObject("usage"));
if (usage != null) {
usageRef.set(usage);
}
if ("conversation.chat.failed".equals(eventType) || "conversation.chat.requires_action".equals(eventType)) {
throw new AgentFlowException("Coze chat status event: " + eventType);
}
} else if (eventType != null && eventType.startsWith("conversation.message.")) {
conversationIdRef.set(firstNonBlank(
payload == null ? null : payload.getString("conversation_id"),
conversationIdRef.get()
));
chatIdRef.set(firstNonBlank(
payload == null ? null : payload.getString("chat_id"),
chatIdRef.get()
));
messageIdRef.set(firstNonBlank(
payload == null ? null : payload.getString("id"),
messageIdRef.get()
));
String contentValue = payload == null ? null : payload.getString("content");
if ("conversation.message.delta".equals(eventType)) {
delta = contentValue;
if (!isBlank(delta)) {
content.append(delta);
sawDelta.set(true);
}
} else if ("conversation.message.completed".equals(eventType)) {
if (!sawDelta.get() && !isBlank(contentValue)) {
content.append(contentValue);
}
}
}
AgentFlowChatEvent event = AgentFlowChatEvent.builder()
.type(eventType)
.contentDelta(delta)
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(chatIdRef.get())
.usage(usageRef.get())
.raw(payload == null ? data : payload)
.build();
listener.onEvent(event);
traceEvent(traceContext, event);
} catch (Throwable ex) {
failure.set(ex);
traceError(traceContext, ex);
listener.onError(ex);
closed.set(true);
eventSource.cancel();
latch.countDown();
}
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
if (closed.compareAndSet(false, true)) {
AgentFlowChatResponse responsePayload = completionRef.get();
if (responsePayload == null) {
responsePayload = AgentFlowChatResponse.builder()
.content(content.toString())
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(chatIdRef.get())
.usage(usageRef.get())
.build();
completionRef.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
}
latch.countDown();
}
}
@Override
public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
Throwable error = t;
if (error == null && response != null) {
error = new AgentFlowException("Coze stream failed: HTTP " + response.code());
}
if (error == null) {
error = new AgentFlowException("Coze stream failed");
}
failure.set(error);
traceError(traceContext, error);
listener.onError(error);
closed.set(true);
latch.countDown();
}
});
if (!latch.await(pollTimeoutMillis(), TimeUnit.MILLISECONDS)) {
throw new AgentFlowException("Coze stream timed out");
}
if (failure.get() != null) {
if (failure.get() instanceof Exception) {
throw (Exception) failure.get();
}
throw new AgentFlowException("Coze stream failed", failure.get());
}
}
private JSONObject pollChat(String conversationId, String chatId) throws Exception {
long deadline = System.currentTimeMillis() + pollTimeoutMillis();
while (true) {
String url = appendQuery(
joinedUrl(requireBaseUrl(), "v3/chat/retrieve"),
query(conversationId, chatId)
);
JSONObject retrieveResponse = executeObject(jsonRequestBuilder(url).get().build());
assertCozeSuccess(retrieveResponse);
JSONObject chatData = retrieveResponse.getJSONObject("data");
String status = chatData == null ? null : chatData.getString("status");
if ("completed".equals(status) || "failed".equals(status) || "canceled".equals(status) || "requires_action".equals(status)) {
return chatData == null ? new JSONObject() : chatData;
}
if (System.currentTimeMillis() >= deadline) {
throw new AgentFlowException("Coze chat poll timed out");
}
sleep(pollIntervalMillis());
}
}
private Request buildCreateRequest(AgentFlowChatRequest request, boolean stream) {
String conversationId = defaultConversationId(request.getConversationId());
String url = appendQuery(
joinedUrl(requireBaseUrl(), "v3/chat"),
queryConversation(conversationId)
);
return jsonRequestBuilder(url).post(jsonBody(buildCreateBody(request, stream))).build();
}
private JSONObject buildCreateBody(AgentFlowChatRequest request, boolean stream) {
JSONObject body = new JSONObject();
body.put("bot_id", requireBotId());
body.put("user_id", defaultUserId(request.getUserId()));
body.put("stream", stream);
body.put("additional_messages", Collections.singletonList(userMessage(request.getPrompt())));
if (request.getInputs() != null && !request.getInputs().isEmpty()) {
body.put("parameters", request.getInputs());
}
if (request.getMetadata() != null && !request.getMetadata().isEmpty()) {
body.put("meta_data", toStringMap(request.getMetadata()));
}
if (!stream && !body.containsKey("auto_save_history")) {
body.put("auto_save_history", true);
}
if (request.getExtraBody() != null && !request.getExtraBody().isEmpty()) {
body.putAll(request.getExtraBody());
}
return body;
}
private Request buildMessageListRequest(String conversationId, String chatId) {
String url = appendQuery(
joinedUrl(requireBaseUrl(), "v1/conversation/message/list"),
queryConversation(conversationId)
);
JSONObject body = new JSONObject();
body.put("conversation_id", conversationId);
body.put("chat_id", chatId);
body.put("order", "asc");
body.put("limit", 50);
if (!isBlank(agentFlowConfig.getBotId())) {
body.put("bot_id", agentFlowConfig.getBotId());
}
return jsonRequestBuilder(url).post(jsonBody(body)).build();
}
private JSONObject userMessage(String prompt) {
JSONObject message = new JSONObject();
message.put("role", "user");
message.put("type", "question");
message.put("content", prompt);
message.put("content_type", "text");
return message;
}
private JSONObject parseObjectOrNull(String data) {
if (isBlank(data)) {
return null;
}
try {
Object parsed = JSON.parse(data);
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
} catch (Exception ex) {
return null;
}
}
private Map queryConversation(String conversationId) {
Map values = new LinkedHashMap();
if (!isBlank(conversationId)) {
values.put("conversation_id", conversationId);
}
return values;
}
private Map query(String conversationId, String chatId) {
Map values = queryConversation(conversationId);
if (!isBlank(chatId)) {
values.put("chat_id", chatId);
}
return values;
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/chat/DifyAgentFlowChatService.java
================================================
package io.github.lnyocly.ai4j.agentflow.chat;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.github.lnyocly.ai4j.agentflow.AgentFlowConfig;
import io.github.lnyocly.ai4j.agentflow.AgentFlowException;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import io.github.lnyocly.ai4j.agentflow.support.AgentFlowSupport;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceContext;
import io.github.lnyocly.ai4j.service.Configuration;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class DifyAgentFlowChatService extends AgentFlowSupport implements AgentFlowChatService {
public DifyAgentFlowChatService(Configuration configuration, AgentFlowConfig agentFlowConfig) {
super(configuration, agentFlowConfig);
}
@Override
public AgentFlowChatResponse chat(AgentFlowChatRequest request) throws Exception {
AgentFlowTraceContext traceContext = startTrace("chat", false, request);
try {
JSONObject body = buildRequestBody(request, "blocking");
String url = joinedUrl(requireBaseUrl(), "v1/chat-messages");
JSONObject response = executeObject(jsonRequestBuilder(url).post(jsonBody(body)).build());
AgentFlowChatResponse chatResponse = mapBlockingResponse(response);
traceComplete(traceContext, chatResponse);
return chatResponse;
} catch (Exception ex) {
traceError(traceContext, ex);
throw ex;
}
}
@Override
public void chatStream(AgentFlowChatRequest request, final AgentFlowChatListener listener) throws Exception {
if (listener == null) {
throw new IllegalArgumentException("listener is required");
}
final AgentFlowTraceContext traceContext = startTrace("chat", true, request);
JSONObject body = buildRequestBody(request, "streaming");
String url = joinedUrl(requireBaseUrl(), "v1/chat-messages");
Request httpRequest = jsonRequestBuilder(url).post(jsonBody(body)).build();
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference failure = new AtomicReference();
final AtomicReference completion = new AtomicReference();
final AtomicReference conversationIdRef = new AtomicReference();
final AtomicReference messageIdRef = new AtomicReference();
final AtomicReference taskIdRef = new AtomicReference();
final AtomicReference usageRef = new AtomicReference();
final StringBuilder content = new StringBuilder();
final AtomicBoolean closed = new AtomicBoolean(false);
eventSourceFactory.newEventSource(httpRequest, new EventSourceListener() {
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
listener.onOpen();
}
@Override
public void onEvent(@NotNull EventSource eventSource,
@Nullable String id,
@Nullable String type,
@NotNull String data) {
try {
JSONObject payload = parseObjectOrNull(data);
String eventType = firstNonBlank(type, payload == null ? null : payload.getString("event"));
if (isBlank(eventType) || "ping".equals(eventType)) {
return;
}
String conversationId = payload == null ? null : payload.getString("conversation_id");
String messageId = payload == null ? null : firstNonBlank(payload.getString("message_id"), payload.getString("id"));
String taskId = payload == null ? null : payload.getString("task_id");
if (!isBlank(conversationId)) {
conversationIdRef.set(conversationId);
}
if (!isBlank(messageId)) {
messageIdRef.set(messageId);
}
if (!isBlank(taskId)) {
taskIdRef.set(taskId);
}
AgentFlowUsage usage = payload == null ? null : usageFromDify(metadataUsage(payload));
if (usage != null) {
usageRef.set(usage);
}
String delta = null;
if ("message".equals(eventType) || "agent_message".equals(eventType)) {
delta = payload == null ? null : payload.getString("answer");
if (!isBlank(delta)) {
content.append(delta);
}
}
boolean done = "message_end".equals(eventType);
AgentFlowChatEvent event = AgentFlowChatEvent.builder()
.type(eventType)
.contentDelta(delta)
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(taskIdRef.get())
.done(done)
.usage(usageRef.get())
.raw(payload == null ? data : payload)
.build();
listener.onEvent(event);
traceEvent(traceContext, event);
if ("error".equals(eventType)) {
throw new AgentFlowException("Dify stream error: " + (payload == null ? data : payload.toJSONString()));
}
if (done) {
AgentFlowChatResponse responsePayload = AgentFlowChatResponse.builder()
.content(content.toString())
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(taskIdRef.get())
.usage(usageRef.get())
.raw(payload)
.build();
completion.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
closed.set(true);
eventSource.cancel();
latch.countDown();
}
} catch (Throwable ex) {
failure.set(ex);
traceError(traceContext, ex);
listener.onError(ex);
closed.set(true);
eventSource.cancel();
latch.countDown();
}
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
if (closed.compareAndSet(false, true)) {
AgentFlowChatResponse responsePayload = completion.get();
if (responsePayload == null) {
responsePayload = AgentFlowChatResponse.builder()
.content(content.toString())
.conversationId(conversationIdRef.get())
.messageId(messageIdRef.get())
.taskId(taskIdRef.get())
.usage(usageRef.get())
.build();
completion.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
}
latch.countDown();
}
}
@Override
public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
Throwable error = t;
if (error == null && response != null) {
error = new AgentFlowException("Dify stream failed: HTTP " + response.code());
}
if (error == null) {
error = new AgentFlowException("Dify stream failed");
}
failure.set(error);
traceError(traceContext, error);
listener.onError(error);
closed.set(true);
latch.countDown();
}
});
if (!latch.await(pollTimeoutMillis(), TimeUnit.MILLISECONDS)) {
throw new AgentFlowException("Dify stream timed out");
}
if (failure.get() != null) {
if (failure.get() instanceof Exception) {
throw (Exception) failure.get();
}
throw new AgentFlowException("Dify stream failed", failure.get());
}
}
private JSONObject buildRequestBody(AgentFlowChatRequest request, String responseMode) {
JSONObject body = new JSONObject();
body.put("query", request.getPrompt());
body.put("inputs", request.getInputs() == null ? Collections.emptyMap() : request.getInputs());
body.put("user", defaultUserId(request.getUserId()));
body.put("response_mode", responseMode);
String conversationId = defaultConversationId(request.getConversationId());
if (!isBlank(conversationId)) {
body.put("conversation_id", conversationId);
}
if (request.getExtraBody() != null && !request.getExtraBody().isEmpty()) {
body.putAll(request.getExtraBody());
}
return body;
}
private AgentFlowChatResponse mapBlockingResponse(JSONObject response) {
return AgentFlowChatResponse.builder()
.content(firstNonBlank(response.getString("answer"), response.getString("message")))
.conversationId(response.getString("conversation_id"))
.messageId(firstNonBlank(response.getString("message_id"), response.getString("id")))
.taskId(response.getString("task_id"))
.usage(usageFromDify(metadataUsage(response)))
.raw(response)
.build();
}
private JSONObject metadataUsage(JSONObject payload) {
JSONObject metadata = payload == null ? null : payload.getJSONObject("metadata");
return metadata == null ? null : metadata.getJSONObject("usage");
}
private JSONObject parseObjectOrNull(String data) {
if (isBlank(data)) {
return null;
}
try {
Object parsed = JSON.parse(data);
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
} catch (Exception ex) {
return null;
}
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/support/AgentFlowSupport.java
================================================
package io.github.lnyocly.ai4j.agentflow.support;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.github.lnyocly.ai4j.agentflow.AgentFlowConfig;
import io.github.lnyocly.ai4j.agentflow.AgentFlowException;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceContext;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceListener;
import io.github.lnyocly.ai4j.constant.Constants;
import io.github.lnyocly.ai4j.network.UrlUtils;
import io.github.lnyocly.ai4j.service.Configuration;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public abstract class AgentFlowSupport {
protected static final MediaType JSON_MEDIA_TYPE = MediaType.get(Constants.APPLICATION_JSON);
protected final Configuration configuration;
protected final AgentFlowConfig agentFlowConfig;
protected final OkHttpClient okHttpClient;
protected final EventSource.Factory eventSourceFactory;
protected AgentFlowSupport(Configuration configuration, AgentFlowConfig agentFlowConfig) {
if (configuration == null) {
throw new IllegalArgumentException("configuration is required");
}
if (configuration.getOkHttpClient() == null) {
throw new IllegalArgumentException("OkHttpClient configuration is required");
}
if (agentFlowConfig == null) {
throw new IllegalArgumentException("agentFlowConfig is required");
}
this.configuration = configuration;
this.agentFlowConfig = agentFlowConfig;
this.okHttpClient = configuration.getOkHttpClient();
this.eventSourceFactory = configuration.createRequestFactory();
}
protected String defaultUserId(String requestUserId) {
if (!isBlank(requestUserId)) {
return requestUserId;
}
if (!isBlank(agentFlowConfig.getUserId())) {
return agentFlowConfig.getUserId();
}
return "default-user";
}
protected String defaultConversationId(String requestConversationId) {
if (!isBlank(requestConversationId)) {
return requestConversationId;
}
return agentFlowConfig.getConversationId();
}
protected String requireBaseUrl() {
if (isBlank(agentFlowConfig.getBaseUrl())) {
throw new IllegalArgumentException("baseUrl is required");
}
return agentFlowConfig.getBaseUrl();
}
protected String requireWebhookUrl() {
if (isBlank(agentFlowConfig.getWebhookUrl())) {
throw new IllegalArgumentException("webhookUrl is required");
}
return agentFlowConfig.getWebhookUrl();
}
protected String requireApiKey() {
if (isBlank(agentFlowConfig.getApiKey())) {
throw new IllegalArgumentException("apiKey is required");
}
return agentFlowConfig.getApiKey();
}
protected String requireBotId() {
if (isBlank(agentFlowConfig.getBotId())) {
throw new IllegalArgumentException("botId is required");
}
return agentFlowConfig.getBotId();
}
protected String requireWorkflowId(String requestWorkflowId) {
if (!isBlank(requestWorkflowId)) {
return requestWorkflowId;
}
if (!isBlank(agentFlowConfig.getWorkflowId())) {
return agentFlowConfig.getWorkflowId();
}
throw new IllegalArgumentException("workflowId is required");
}
protected String joinedUrl(String baseUrl, String path) {
return UrlUtils.concatUrl(baseUrl, path);
}
protected String appendQuery(String url, Map queryParameters) {
HttpUrl parsed = HttpUrl.parse(url);
if (parsed == null) {
throw new IllegalArgumentException("Invalid URL: " + url);
}
HttpUrl.Builder builder = parsed.newBuilder();
if (queryParameters != null) {
for (Map.Entry entry : queryParameters.entrySet()) {
if (!isBlank(entry.getValue())) {
builder.addQueryParameter(entry.getKey(), entry.getValue());
}
}
}
return builder.build().toString();
}
protected RequestBody jsonBody(Object body) {
return RequestBody.create(JSON.toJSONString(body), JSON_MEDIA_TYPE);
}
protected Request.Builder jsonRequestBuilder(String url) {
Request.Builder builder = new Request.Builder().url(url);
builder.header("Content-Type", Constants.APPLICATION_JSON);
if (!isBlank(agentFlowConfig.getApiKey())) {
builder.header("Authorization", "Bearer " + agentFlowConfig.getApiKey());
}
if (agentFlowConfig.getHeaders() != null) {
for (Map.Entry entry : agentFlowConfig.getHeaders().entrySet()) {
if (!isBlank(entry.getKey()) && entry.getValue() != null) {
builder.header(entry.getKey(), entry.getValue());
}
}
}
return builder;
}
protected String execute(Request request) throws IOException {
try (Response response = okHttpClient.newCall(request).execute()) {
return readResponse(request, response);
}
}
protected JSONObject executeObject(Request request) throws IOException {
String body = execute(request);
if (isBlank(body)) {
return new JSONObject();
}
Object parsed = JSON.parse(body);
if (parsed instanceof JSONObject) {
return (JSONObject) parsed;
}
throw new AgentFlowException("Expected JSON object response but got: " + body);
}
protected Object parseJsonOrText(String body) {
if (isBlank(body)) {
return null;
}
try {
return JSON.parse(body);
} catch (Exception ex) {
return body;
}
}
protected String readResponse(Request request, Response response) throws IOException {
ResponseBody body = response.body();
String content = body == null ? "" : body.string();
if (!response.isSuccessful()) {
throw new AgentFlowException("HTTP " + response.code() + " calling " + request.url() + ": " + abbreviate(content));
}
return content;
}
protected void assertCozeSuccess(JSONObject response) {
Integer code = response == null ? null : response.getInteger("code");
if (code != null && code.intValue() != 0) {
throw new AgentFlowException("Coze request failed: code=" + code + ", msg=" + response.getString("msg"));
}
}
protected Map mutableMap(Map source) {
if (source == null || source.isEmpty()) {
return new LinkedHashMap();
}
return new LinkedHashMap(source);
}
protected Map toStringMap(Map source) {
if (source == null || source.isEmpty()) {
return Collections.emptyMap();
}
Map result = new LinkedHashMap();
for (Map.Entry entry : source.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
result.put(entry.getKey(), String.valueOf(entry.getValue()));
}
}
return result;
}
protected String extractText(Object value) {
if (value == null) {
return null;
}
if (value instanceof String) {
return (String) value;
}
if (value instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) value;
String direct = firstNonBlank(
jsonObject.getString("answer"),
jsonObject.getString("output"),
jsonObject.getString("text"),
jsonObject.getString("content"),
jsonObject.getString("result"),
jsonObject.getString("message")
);
if (!isBlank(direct)) {
return direct;
}
if (jsonObject.size() == 1) {
Map.Entry entry = jsonObject.entrySet().iterator().next();
return extractText(entry.getValue());
}
return JSON.toJSONString(jsonObject);
}
return String.valueOf(value);
}
protected AgentFlowUsage usageFromDify(JSONObject usage) {
if (usage == null || usage.isEmpty()) {
return null;
}
return AgentFlowUsage.builder()
.inputTokens(usage.getInteger("prompt_tokens"))
.outputTokens(usage.getInteger("completion_tokens"))
.totalTokens(usage.getInteger("total_tokens"))
.raw(usage)
.build();
}
protected AgentFlowUsage usageFromCoze(JSONObject usage) {
if (usage == null || usage.isEmpty()) {
return null;
}
return AgentFlowUsage.builder()
.inputTokens(firstNonNullInteger(usage.getInteger("input_tokens"), usage.getInteger("input_count")))
.outputTokens(firstNonNullInteger(usage.getInteger("output_tokens"), usage.getInteger("output_count")))
.totalTokens(usage.getInteger("token_count"))
.raw(usage)
.build();
}
protected Integer firstNonNullInteger(Integer first, Integer second) {
return first != null ? first : second;
}
protected String firstNonBlank(String first, String second) {
return firstNonBlank(first, second, null, null, null, null);
}
protected String firstNonBlank(String first,
String second,
String third,
String fourth,
String fifth,
String sixth) {
String[] values = new String[]{first, second, third, fourth, fifth, sixth};
for (String value : values) {
if (!isBlank(value)) {
return value;
}
}
return null;
}
protected long pollIntervalMillis() {
Long value = agentFlowConfig.getPollIntervalMillis();
return value == null || value.longValue() <= 0L ? 1_000L : value.longValue();
}
protected long pollTimeoutMillis() {
Long value = agentFlowConfig.getPollTimeoutMillis();
return value == null || value.longValue() <= 0L ? 60_000L : value.longValue();
}
protected void sleep(long millis) throws InterruptedException {
if (millis > 0L) {
Thread.sleep(millis);
}
}
protected String abbreviate(String value) {
if (value == null) {
return null;
}
if (value.length() <= 500) {
return value;
}
return value.substring(0, 500) + "...";
}
protected boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
protected AgentFlowTraceContext startTrace(String operation, boolean streaming, Object request) {
AgentFlowTraceContext context = AgentFlowTraceContext.builder()
.executionId(UUID.randomUUID().toString())
.type(agentFlowConfig.getType())
.operation(operation)
.streaming(streaming)
.startedAt(System.currentTimeMillis())
.baseUrl(agentFlowConfig.getBaseUrl())
.webhookUrl(agentFlowConfig.getWebhookUrl())
.botId(agentFlowConfig.getBotId())
.workflowId(agentFlowConfig.getWorkflowId())
.appId(agentFlowConfig.getAppId())
.configuredUserId(agentFlowConfig.getUserId())
.configuredConversationId(agentFlowConfig.getConversationId())
.request(request)
.build();
notifyTraceStart(context);
return context;
}
protected void traceEvent(AgentFlowTraceContext context, Object event) {
List listeners = traceListeners();
if (context == null || event == null || listeners.isEmpty()) {
return;
}
for (AgentFlowTraceListener listener : listeners) {
if (listener == null) {
continue;
}
try {
listener.onEvent(context, event);
} catch (Throwable ignored) {
// Trace listeners must never break the primary AgentFlow call path.
}
}
}
protected void traceComplete(AgentFlowTraceContext context, Object response) {
List listeners = traceListeners();
if (context == null || listeners.isEmpty()) {
return;
}
for (AgentFlowTraceListener listener : listeners) {
if (listener == null) {
continue;
}
try {
listener.onComplete(context, response);
} catch (Throwable ignored) {
// Trace listeners must never break the primary AgentFlow call path.
}
}
}
protected void traceError(AgentFlowTraceContext context, Throwable throwable) {
List listeners = traceListeners();
if (context == null || throwable == null || listeners.isEmpty()) {
return;
}
for (AgentFlowTraceListener listener : listeners) {
if (listener == null) {
continue;
}
try {
listener.onError(context, throwable);
} catch (Throwable ignored) {
// Trace listeners must never break the primary AgentFlow call path.
}
}
}
private void notifyTraceStart(AgentFlowTraceContext context) {
List listeners = traceListeners();
if (context == null || listeners.isEmpty()) {
return;
}
for (AgentFlowTraceListener listener : listeners) {
if (listener == null) {
continue;
}
try {
listener.onStart(context);
} catch (Throwable ignored) {
// Trace listeners must never break the primary AgentFlow call path.
}
}
}
private List traceListeners() {
List listeners = agentFlowConfig.getTraceListeners();
return listeners == null ? Collections.emptyList() : listeners;
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/trace/AgentFlowTraceContext.java
================================================
package io.github.lnyocly.ai4j.agentflow.trace;
import io.github.lnyocly.ai4j.agentflow.AgentFlowType;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowTraceContext {
private String executionId;
private AgentFlowType type;
private String operation;
private boolean streaming;
private long startedAt;
private String baseUrl;
private String webhookUrl;
private String botId;
private String workflowId;
private String appId;
private String configuredUserId;
private String configuredConversationId;
private Object request;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/trace/AgentFlowTraceListener.java
================================================
package io.github.lnyocly.ai4j.agentflow.trace;
public interface AgentFlowTraceListener {
default void onStart(AgentFlowTraceContext context) {
}
default void onEvent(AgentFlowTraceContext context, Object event) {
}
default void onComplete(AgentFlowTraceContext context, Object response) {
}
default void onError(AgentFlowTraceContext context, Throwable throwable) {
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/AgentFlowWorkflowEvent.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.Map;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowWorkflowEvent {
private String type;
private String status;
private String outputText;
@Builder.Default
private Map outputs = Collections.emptyMap();
private String taskId;
private String workflowRunId;
private boolean done;
private AgentFlowUsage usage;
private Object raw;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/AgentFlowWorkflowListener.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
public interface AgentFlowWorkflowListener {
void onEvent(AgentFlowWorkflowEvent event);
default void onOpen() {
}
default void onError(Throwable throwable) {
}
default void onComplete(AgentFlowWorkflowResponse response) {
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/AgentFlowWorkflowRequest.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.Map;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowWorkflowRequest {
@Builder.Default
private Map inputs = Collections.emptyMap();
private String userId;
private String workflowId;
@Builder.Default
private Map metadata = Collections.emptyMap();
@Builder.Default
private Map extraBody = Collections.emptyMap();
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/AgentFlowWorkflowResponse.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.Map;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentFlowWorkflowResponse {
private String status;
private String outputText;
@Builder.Default
private Map outputs = Collections.emptyMap();
private String taskId;
private String workflowRunId;
private AgentFlowUsage usage;
private Object raw;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/AgentFlowWorkflowService.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
public interface AgentFlowWorkflowService {
AgentFlowWorkflowResponse run(AgentFlowWorkflowRequest request) throws Exception;
void runStream(AgentFlowWorkflowRequest request, AgentFlowWorkflowListener listener) throws Exception;
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/CozeAgentFlowWorkflowService.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.github.lnyocly.ai4j.agentflow.AgentFlowConfig;
import io.github.lnyocly.ai4j.agentflow.AgentFlowException;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import io.github.lnyocly.ai4j.agentflow.support.AgentFlowSupport;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceContext;
import io.github.lnyocly.ai4j.service.Configuration;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class CozeAgentFlowWorkflowService extends AgentFlowSupport implements AgentFlowWorkflowService {
public CozeAgentFlowWorkflowService(Configuration configuration, AgentFlowConfig agentFlowConfig) {
super(configuration, agentFlowConfig);
}
@Override
public AgentFlowWorkflowResponse run(AgentFlowWorkflowRequest request) throws Exception {
AgentFlowTraceContext traceContext = startTrace("workflow", false, request);
try {
JSONObject response = executeObject(buildRunRequest(request, false));
assertCozeSuccess(response);
Object dataValue = response.get("data");
Object parsedData = parseWorkflowData(dataValue);
Map outputs = parsedData instanceof JSONObject
? new LinkedHashMap((JSONObject) parsedData)
: Collections.emptyMap();
AgentFlowWorkflowResponse workflowResponse = AgentFlowWorkflowResponse.builder()
.status("completed")
.outputText(extractText(parsedData))
.outputs(outputs)
.workflowRunId(response.getString("execute_id"))
.usage(usageFromCoze(response.getJSONObject("usage")))
.raw(response)
.build();
traceComplete(traceContext, workflowResponse);
return workflowResponse;
} catch (Exception ex) {
traceError(traceContext, ex);
throw ex;
}
}
@Override
public void runStream(AgentFlowWorkflowRequest request, final AgentFlowWorkflowListener listener) throws Exception {
if (listener == null) {
throw new IllegalArgumentException("listener is required");
}
final AgentFlowTraceContext traceContext = startTrace("workflow", true, request);
Request httpRequest = buildRunRequest(request, true);
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference failure = new AtomicReference();
final AtomicReference completion = new AtomicReference();
final AtomicReference usageRef = new AtomicReference();
final StringBuilder content = new StringBuilder();
final AtomicBoolean closed = new AtomicBoolean(false);
eventSourceFactory.newEventSource(httpRequest, new EventSourceListener() {
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
listener.onOpen();
}
@Override
public void onEvent(@NotNull EventSource eventSource,
@Nullable String id,
@Nullable String type,
@NotNull String data) {
try {
String eventType = type;
JSONObject payload = parseObjectOrNull(data);
boolean done = false;
String outputText = null;
if ("Message".equals(eventType)) {
outputText = payload == null ? null : payload.getString("content");
if (!isBlank(outputText)) {
content.append(outputText);
}
AgentFlowUsage usage = payload == null ? null : usageFromCoze(payload.getJSONObject("usage"));
if (usage != null) {
usageRef.set(usage);
}
} else if ("Interrupt".equals(eventType)) {
throw new AgentFlowException("Coze workflow interrupted: " + data);
} else if ("Error".equals(eventType)) {
throw new AgentFlowException("Coze workflow stream error: " + data);
} else if ("Done".equals(eventType)) {
done = true;
}
AgentFlowWorkflowEvent event = AgentFlowWorkflowEvent.builder()
.type(eventType)
.status(done ? "completed" : null)
.outputText(outputText)
.done(done)
.usage(usageRef.get())
.raw(payload == null ? data : payload)
.build();
listener.onEvent(event);
traceEvent(traceContext, event);
if (done) {
AgentFlowWorkflowResponse responsePayload = AgentFlowWorkflowResponse.builder()
.status("completed")
.outputText(content.toString())
.usage(usageRef.get())
.raw(payload == null ? data : payload)
.build();
completion.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
closed.set(true);
eventSource.cancel();
latch.countDown();
}
} catch (Throwable ex) {
failure.set(ex);
traceError(traceContext, ex);
listener.onError(ex);
closed.set(true);
eventSource.cancel();
latch.countDown();
}
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
if (closed.compareAndSet(false, true)) {
AgentFlowWorkflowResponse responsePayload = completion.get();
if (responsePayload == null) {
responsePayload = AgentFlowWorkflowResponse.builder()
.status("completed")
.outputText(content.toString())
.usage(usageRef.get())
.build();
completion.set(responsePayload);
listener.onComplete(responsePayload);
traceComplete(traceContext, responsePayload);
}
latch.countDown();
}
}
@Override
public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
Throwable error = t;
if (error == null && response != null) {
error = new AgentFlowException("Coze workflow stream failed: HTTP " + response.code());
}
if (error == null) {
error = new AgentFlowException("Coze workflow stream failed");
}
failure.set(error);
traceError(traceContext, error);
listener.onError(error);
closed.set(true);
latch.countDown();
}
});
if (!latch.await(pollTimeoutMillis(), TimeUnit.MILLISECONDS)) {
throw new AgentFlowException("Coze workflow stream timed out");
}
if (failure.get() != null) {
if (failure.get() instanceof Exception) {
throw (Exception) failure.get();
}
throw new AgentFlowException("Coze workflow stream failed", failure.get());
}
}
private Request buildRunRequest(AgentFlowWorkflowRequest request, boolean stream) {
String path = stream ? "v1/workflow/stream_run" : "v1/workflow/run";
String url = joinedUrl(requireBaseUrl(), path);
return jsonRequestBuilder(url).post(jsonBody(buildRequestBody(request))).build();
}
private JSONObject buildRequestBody(AgentFlowWorkflowRequest request) {
JSONObject body = new JSONObject();
body.put("workflow_id", requireWorkflowId(request.getWorkflowId()));
body.put("parameters", request.getInputs() == null ? Collections.emptyMap() : request.getInputs());
if (!isBlank(agentFlowConfig.getBotId())) {
body.put("bot_id", agentFlowConfig.getBotId());
}
if (!isBlank(agentFlowConfig.getAppId())) {
body.put("app_id", agentFlowConfig.getAppId());
}
if (request.getMetadata() != null && !request.getMetadata().isEmpty()) {
body.put("ext", toStringMap(request.getMetadata()));
}
if (request.getExtraBody() != null && !request.getExtraBody().isEmpty()) {
body.putAll(request.getExtraBody());
}
return body;
}
private Object parseWorkflowData(Object dataValue) {
if (dataValue == null) {
return null;
}
if (!(dataValue instanceof String)) {
return dataValue;
}
String text = (String) dataValue;
if (isBlank(text)) {
return null;
}
try {
return JSON.parse(text);
} catch (Exception ex) {
return text;
}
}
private JSONObject parseObjectOrNull(String data) {
if (isBlank(data)) {
return null;
}
try {
Object parsed = JSON.parse(data);
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
} catch (Exception ex) {
return null;
}
}
}
================================================
FILE: ai4j/src/main/java/io/github/lnyocly/ai4j/agentflow/workflow/DifyAgentFlowWorkflowService.java
================================================
package io.github.lnyocly.ai4j.agentflow.workflow;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.github.lnyocly.ai4j.agentflow.AgentFlowConfig;
import io.github.lnyocly.ai4j.agentflow.AgentFlowException;
import io.github.lnyocly.ai4j.agentflow.AgentFlowUsage;
import io.github.lnyocly.ai4j.agentflow.support.AgentFlowSupport;
import io.github.lnyocly.ai4j.agentflow.trace.AgentFlowTraceContext;
import io.github.lnyocly.ai4j.service.Configuration;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class DifyAgentFlowWorkflowService extends AgentFlowSupport implements AgentFlowWorkflowService {
public DifyAgentFlowWorkflowService(Configuration configuration, AgentFlowConfig agentFlowConfig) {
super(configuration, agentFlowConfig);
}
@Override
public AgentFlowWorkflowResponse run(AgentFlowWorkflowRequest request) throws Exception {
AgentFlowTraceContext traceContext = startTrace("workflow", false, request);
try {
JSONObject body = buildRequestBody(request, "blocking");
String url = joinedUrl(requireBaseUrl(), "v1/workflows/run");
JSONObject response = executeObject(jsonRequestBuilder(url).post(jsonBody(body)).build());
AgentFlowWorkflowResponse workflowResponse = mapWorkflowResponse(response);
traceComplete(traceContext, workflowResponse);
return workflowResponse;
} catch (Exception ex) {
traceError(traceContext, ex);
throw ex;
}
}
@Override
public void runStream(AgentFlowWorkflowRequest request, final AgentFlowWorkflowListener listener) throws Exception {
if (listener == null) {
throw new IllegalArgumentException("listener is required");
}
final AgentFlowTraceContext traceContext = startTrace("workflow", true, request);
JSONObject body = buildRequestBody(request, "streaming");
String url = joinedUrl(requireBaseUrl(), "v1/workflows/run");
Request httpRequest = jsonRequestBuilder(url).post(jsonBody(body)).build();
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference failure = new AtomicReference();
final AtomicReference completion = new AtomicReference();
final AtomicReference taskIdRef = new AtomicReference();
final AtomicReference workflowRunIdRef = new AtomicReference();
final AtomicReference usageRef = new AtomicReference();
final AtomicReference