diff --git a/README.md b/README.md index 2de261db2..6dc71d9fa 100644 --- a/README.md +++ b/README.md @@ -79,44 +79,22 @@ AG-UI is complementary to the other 2 top agentic protocols AG-UI was born from CopilotKit's initial partnership with LangGraph and CrewAI - and brings the incredibly popular agent-user-interactivity infrastructure to the wider agentic ecosystem. -## Frameworks - -| Framework | Status | AG-UI Resources | -| ------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------- | -| Direct to LLM | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/direct-to-llm) | - -#### 🤝 Partnerships -| Framework | Status | AG-UI Resources | -| ---------- | ------- | ---------------- | -| [LangGraph](https://www.langchain.com/langgraph) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/langgraph/) 🎮 [Demos](https://dojo.ag-ui.com/langgraph-fastapi/feature/shared_state) | -| [Google ADK](https://google.github.io/adk-docs/get-started/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/adk) 🎮 [Demos](https://dojo.ag-ui.com/adk-middleware) | -| [CrewAI](https://crewai.com/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/crewai-flows) 🎮 [Demos](https://dojo.ag-ui.com/crewai/feature/shared_state) | - -#### 🧩 1st Party -| Framework | Status | AG-UI Resources | -| ---------- | ------- | ---------------- | -| [Mastra](https://mastra.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/mastra/) 🎮 [Demos](https://dojo.ag-ui.com/mastra) | -| [Pydantic AI](https://github.com/pydantic/pydantic-ai) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/pydantic-ai/) 🎮 [Demos](https://dojo.ag-ui.com/pydantic-ai/feature/shared_state) | -| [Agno](https://github.com/agno-agi/agno) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/agno/) 🎮 [Demos](https://dojo.ag-ui.com/agno) | -| [LlamaIndex](https://github.com/run-llama/llama_index) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/llamaindex/) 🎮 [Demos](https://dojo.ag-ui.com/llamaindex/feature/shared_state) | -| [AG2](https://ag2.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/ag2/) | -| [AWS Bedrock Agents](https://aws.amazon.com/bedrock/agents/) | 🛠️ In Progress | – | -| [AWS Strands Agents](https://github.com/strands-agents/sdk-python) | 🛠️ In Progress | – | -| [Microsoft Agent Framework](https://azure.microsoft.com/en-us/blog/introducing-microsoft-agent-framework/) | 🛠️ In Progress | – | - -#### 🌐 Community -| Framework | Status | AG-UI Resources | -| ---------- | ------- | ---------------- | -| [Vercel AI SDK](https://github.com/vercel/ai) | ✅ Supported | ➡️ [Docs](https://github.com/ag-ui-protocol/ag-ui/tree/main/integrations/vercel-ai-sdk/typescript) | -| [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) | 🛠️ In Progress | – | -| [Cloudflare Agents](https://developers.cloudflare.com/agents/) | 🛠️ In Progress | – | - - -## Protocols - -| Protocols | Status | AG-UI Resources | Integrations | -| ---------- | ------- | ---------------- | ------------- | -| [A2A]() | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/a2a-protocol) | Partnership | +| Framework | Status | AG-UI Resources | Integrations | +| ------------------------------------------------------------------ | ------------------------ | ---------------------------------------------------------------------------- | ------------------------ | +| No-framework | ✅ Supported | ➡️ Docs coming soon | | +| [LangGraph](https://www.langchain.com/langgraph) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/langgraph/) 🎮 [Demos](https://dojo.ag-ui.com/langgraph-fastapi/feature/shared_state) | Partnership | +| [Mastra](https://mastra.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/mastra/) 🎮 [Demos](https://dojo.ag-ui.com/mastra) | 1st party | +| [Pydantic AI](https://github.com/pydantic/pydantic-ai) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/pydantic-ai/) 🎮 [Demos](https://dojo.ag-ui.com/pydantic-ai/feature/shared_state) | 1st party | +| [Google ADK](https://google.github.io/adk-docs/get-started/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/adk) 🎮 [Demos](https://dojo.ag-ui.com/adk-middleware) | Partnership | +| [Agno](https://github.com/agno-agi/agno) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/agno/) 🎮 [Demos](https://dojo.ag-ui.com/agno) | 1st party | +| [LlamaIndex](https://github.com/run-llama/llama_index) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/llamaindex/) 🎮 [Demos](https://dojo.ag-ui.com/llamaindex/feature/shared_state) | 1st party | +| [CrewAI](https://crewai.com/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/crewai-flows) 🎮 [Demos](https://dojo.ag-ui.com/crewai/feature/shared_state) | Partnership | +| [AG2](https://ag2.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/ag2/) | 1st party | +| [AWS Bedrock Agents](https://aws.amazon.com/bedrock/agents/) | 🛠️ In Progress | – | 1st party | +| [AWS Strands Agents](https://github.com/strands-agents/sdk-python) | 🛠️ In Progress | – | 1st Party | +| [Vercel AI SDK](https://github.com/vercel/ai) | 🛠️ In Progress | – | Community | +| [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) | 🛠️ In Progress | – | Community | +| [Flowise](https://flowiseai.com/) | ✅ Supported | ➡️ Docs coming soon 🎮 Demos coming soon | Community | --- diff --git a/typescript-sdk/integrations/flowise/.npmignore b/typescript-sdk/integrations/flowise/.npmignore new file mode 100644 index 000000000..e568a14e7 --- /dev/null +++ b/typescript-sdk/integrations/flowise/.npmignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +examples/ +src/ +*.ts +!dist/ +!.npmignore \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/README.md b/typescript-sdk/integrations/flowise/README.md new file mode 100644 index 000000000..cbf603e8f --- /dev/null +++ b/typescript-sdk/integrations/flowise/README.md @@ -0,0 +1,38 @@ +# @ag-ui/flowise + +Flowise integration for AG-UI protocol. + +## Installation + +```bash +npm install @ag-ui/flowise +``` + +## Usage + +```typescript +import { FlowiseAgent } from '@ag-ui/flowise'; + +const agent = new FlowiseAgent({ + apiUrl: 'http://localhost:3000/api/v1/prediction/{flowId}', + flowId: 'your-flow-id', + apiKey: 'your-api-key', // Optional +}); + +// Use the agent with AG-UI components +``` + +## API Reference + +### FlowiseAgentConfig + +| Property | Type | Description | +|---------|------|-------------| +| `apiUrl` | string | The Flowise API endpoint URL | +| `flowId` | string | The Flowise flow ID | +| `apiKey` | string (optional) | API key for authentication | +| `headers` | Record (optional) | Additional headers to send with requests | + +## License + +MIT \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/examples/basic-example.ts b/typescript-sdk/integrations/flowise/examples/basic-example.ts new file mode 100644 index 000000000..6833e56a5 --- /dev/null +++ b/typescript-sdk/integrations/flowise/examples/basic-example.ts @@ -0,0 +1,14 @@ +import { FlowiseAgent, FlowiseAgentConfig } from '../src'; + +// Configure the Flowise agent +const config: FlowiseAgentConfig = { + apiUrl: 'http://localhost:3000/api/v1/prediction/{flowId}', + flowId: 'your-flow-id', + apiKey: 'your-api-key', // Optional +}; + +// Create the agent +const agent = new FlowiseAgent(config); + +// Use the agent with AG-UI components +console.log('Flowise agent created successfully!'); \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/jest.config.js b/typescript-sdk/integrations/flowise/jest.config.js new file mode 100644 index 000000000..9a09a4863 --- /dev/null +++ b/typescript-sdk/integrations/flowise/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + moduleDirectories: ['node_modules', 'src'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/package.json b/typescript-sdk/integrations/flowise/package.json new file mode 100644 index 000000000..0849d027a --- /dev/null +++ b/typescript-sdk/integrations/flowise/package.json @@ -0,0 +1,41 @@ +{ + "name": "@ag-ui/flowise", + "author": "Markus Ecker ", + "version": "0.0.1", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist/**", + "README.md" + ], + "private": false, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "test": "jest", + "link:global": "pnpm link --global", + "unlink:global": "pnpm unlink --global" + }, + "peerDependencies": { + "@ag-ui/core": ">=0.0.37", + "@ag-ui/client": ">=0.0.37", + "rxjs": "7.8.1" + }, + "devDependencies": { + "@ag-ui/core": "workspace:*", + "@ag-ui/client": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^20.11.19", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "tsup": "^8.0.2", + "typescript": "^5.3.3" + } +} \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/README.md b/typescript-sdk/integrations/flowise/python/README.md new file mode 100644 index 000000000..1098a62d2 --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/README.md @@ -0,0 +1,40 @@ +# ag-ui-flowise + +Flowise integration for AG-UI protocol. + +## Installation + +```bash +pip install ag-ui-flowise +``` + +## Usage + +```python +from ag_ui_flowise import FlowiseAgent, FlowiseAgentConfig + +config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="your-flow-id", + api_key="your-api-key" # Optional +) + +agent = FlowiseAgent(config) + +# Use the agent with AG-UI components +``` + +## API Reference + +### FlowiseAgentConfig + +| Property | Type | Description | +|---------|------|-------------| +| `api_url` | str | The Flowise API endpoint URL | +| `flow_id` | str | The Flowise flow ID | +| `api_key` | str (optional) | API key for authentication | +| `headers` | Dict[str, str] (optional) | Additional headers to send with requests | + +## License + +MIT \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/ag_ui_flowise/__init__.py b/typescript-sdk/integrations/flowise/python/ag_ui_flowise/__init__.py new file mode 100644 index 000000000..a958d110b --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/ag_ui_flowise/__init__.py @@ -0,0 +1,8 @@ +""" +AG-UI Flowise Integration +""" + +from .flowise_agent import FlowiseAgent, FlowiseAgentConfig + +__all__ = ["FlowiseAgent", "FlowiseAgentConfig"] +__version__ = "0.0.1" \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/ag_ui_flowise/flowise_agent.py b/typescript-sdk/integrations/flowise/python/ag_ui_flowise/flowise_agent.py new file mode 100644 index 000000000..e347b06dc --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/ag_ui_flowise/flowise_agent.py @@ -0,0 +1,187 @@ +""" +Flowise Agent for AG-UI +""" + +import json +import requests +from typing import List, Dict, Any, Optional +from dataclasses import dataclass, asdict +from datetime import datetime + +from ag_ui.core.events import ( + BaseEvent, + EventType, + RunStartedEvent, + RunFinishedEvent, + TextMessageStartEvent, + TextMessageContentEvent, + TextMessageEndEvent, + MessagesSnapshotEvent +) +from ag_ui.core.types import Message + + +@dataclass +class FlowiseAgentConfig: + """Configuration for Flowise Agent""" + api_url: str + flow_id: str + api_key: Optional[str] = None + headers: Optional[Dict[str, str]] = None + + +@dataclass +class FlowiseResponse: + """Response from Flowise API""" + text: str + question: str + chat_id: Optional[str] = None + session_id: Optional[str] = None + source_documents: Optional[List[Dict[str, Any]]] = None + used_tools: Optional[List[Dict[str, Any]]] = None + + +class FlowiseAgent: + """Flowise Agent for AG-UI""" + + def __init__(self, config: FlowiseAgentConfig): + self.config = config + self.api_url = config.api_url.replace('{flowId}', config.flow_id) + + def clone(self): + """Create a clone of this agent""" + return FlowiseAgent(self.config) + + def run(self, input_data: Dict[str, Any]) -> List[BaseEvent]: + """ + Run the Flowise agent + + Args: + input_data: Input data containing messages, threadId, runId, etc. + + Returns: + List of AG-UI events + """ + events: List[BaseEvent] = [] + + try: + # Emit run started event + run_started_event = RunStartedEvent( + type=EventType.RUN_STARTED, + thread_id=input_data.get('threadId', ''), + run_id=input_data.get('runId', '') + ) + events.append(run_started_event) + + # Get the last user message + last_user_message = self._get_last_user_message(input_data.get('messages', [])) + if not last_user_message: + raise ValueError("No user message found") + + # Prepare the request to Flowise + request_body = { + 'question': last_user_message.get('content', ''), + 'history': self._format_history(input_data.get('messages', [])), + 'overrideConfig': { + 'sessionId': input_data.get('threadId', '') + } + } + + # Set up headers + headers = { + 'Content-Type': 'application/json', + **(self.config.headers or {}) + } + + if self.config.api_key: + headers['Authorization'] = f'Bearer {self.config.api_key}' + + # Make the API call to Flowise + response = requests.post( + self.api_url, + headers=headers, + json=request_body, + timeout=30 + ) + + response.raise_for_status() + flowise_response_data = response.json() + + # Create FlowiseResponse object + flowise_response = FlowiseResponse( + text=flowise_response_data.get('text', ''), + question=flowise_response_data.get('question', ''), + chat_id=flowise_response_data.get('chatId'), + session_id=flowise_response_data.get('sessionId'), + source_documents=flowise_response_data.get('sourceDocuments'), + used_tools=flowise_response_data.get('usedTools') + ) + + # Emit text message events + message_id = str(int(datetime.now().timestamp() * 1000)) + + text_message_start_event = TextMessageStartEvent( + type=EventType.TEXT_MESSAGE_START, + message_id=message_id, + role="assistant" + ) + events.append(text_message_start_event) + + text_message_content_event = TextMessageContentEvent( + type=EventType.TEXT_MESSAGE_CONTENT, + message_id=message_id, + delta=flowise_response.text + ) + events.append(text_message_content_event) + + text_message_end_event = TextMessageEndEvent( + type=EventType.TEXT_MESSAGE_END, + message_id=message_id + ) + events.append(text_message_end_event) + + # Emit messages snapshot + messages_snapshot = list(input_data.get('messages', [])) + [{ + 'id': message_id, + 'role': 'assistant', + 'content': flowise_response.text, + 'timestamp': datetime.now().isoformat() + }] + + messages_snapshot_event = MessagesSnapshotEvent( + type=EventType.MESSAGES_SNAPSHOT, + messages=messages_snapshot + ) + events.append(messages_snapshot_event) + + # Emit run finished event + run_finished_event = RunFinishedEvent( + type=EventType.RUN_FINISHED, + thread_id=input_data.get('threadId', ''), + run_id=input_data.get('runId', '') + ) + events.append(run_finished_event) + + except Exception as e: + raise e + + return events + + def _get_last_user_message(self, messages: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """Get the last user message from the messages list""" + # Find the last user message by working backwards from the last message + for i in range(len(messages) - 1, -1, -1): + if messages[i].get('role') == 'user': + return messages[i] + return None + + def _format_history(self, messages: List[Dict[str, Any]]) -> List[Dict[str, str]]: + """Format message history for Flowise API""" + history = [] + for msg in messages: + if msg.get('role') in ['user', 'assistant']: + history.append({ + 'role': 'userMessage' if msg.get('role') == 'user' else 'apiMessage', + 'content': msg.get('content', '') + }) + return history \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/examples/advanced_example.py b/typescript-sdk/integrations/flowise/python/examples/advanced_example.py new file mode 100644 index 000000000..7fc4e048a --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/examples/advanced_example.py @@ -0,0 +1,57 @@ +""" +Advanced example of using Flowise with AG-UI +""" + +import asyncio +from ag_ui_flowise import FlowiseAgent, FlowiseAgentConfig + + +async def main(): + # Configure the Flowise agent + config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="your-flow-id", + api_key="your-api-key", # Optional + headers={ + "Custom-Header": "custom-value" + } + ) + + # Create the agent + agent = FlowiseAgent(config) + + # Prepare input data with conversation history + input_data = { + 'threadId': 'example-thread-id', + 'runId': 'example-run-id', + 'messages': [ + { + 'id': '1', + 'role': 'user', + 'content': 'Hello, how are you?' + }, + { + 'id': '2', + 'role': 'assistant', + 'content': 'I am doing well, thank you for asking!' + }, + { + 'id': '3', + 'role': 'user', + 'content': 'What can you help me with?' + } + ] + } + + # Run the agent + try: + events = agent.run(input_data) + for event in events: + print(f"Event type: {event.type}") + # Process events as needed for your application + except Exception as e: + print(f"Error running agent: {e}") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/examples/basic_example.py b/typescript-sdk/integrations/flowise/python/examples/basic_example.py new file mode 100644 index 000000000..93c684546 --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/examples/basic_example.py @@ -0,0 +1,42 @@ +""" +Basic example of using Flowise with AG-UI +""" + +from ag_ui_flowise import FlowiseAgent, FlowiseAgentConfig + + +def main(): + # Configure the Flowise agent + config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="your-flow-id", + api_key="your-api-key" # Optional + ) + + # Create the agent + agent = FlowiseAgent(config) + + # Prepare input data + input_data = { + 'threadId': 'example-thread-id', + 'runId': 'example-run-id', + 'messages': [ + { + 'id': '1', + 'role': 'user', + 'content': 'Hello, how are you?' + } + ] + } + + # Run the agent + try: + events = agent.run(input_data) + for event in events: + print(f"Event: {event}") + except Exception as e: + print(f"Error running agent: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/pyproject.toml b/typescript-sdk/integrations/flowise/python/pyproject.toml new file mode 100644 index 000000000..812e01c93 --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "ag-ui-flowise" +version = "0.0.1" +description = "Flowise integration for AG-UI protocol" +authors = ["Markus Ecker "] +license = "MIT" +readme = "README.md" +packages = [{include = "ag_ui_flowise"}] + +[tool.poetry.dependencies] +python = "^3.8" +requests = "^2.28.0" +ag-ui = "^0.0.1" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.0.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/python/tests/test_flowise_agent.py b/typescript-sdk/integrations/flowise/python/tests/test_flowise_agent.py new file mode 100644 index 000000000..1b7fad279 --- /dev/null +++ b/typescript-sdk/integrations/flowise/python/tests/test_flowise_agent.py @@ -0,0 +1,77 @@ +""" +Tests for Flowise Agent +""" + +import pytest +from unittest.mock import Mock, patch +from ag_ui_flowise.flowise_agent import FlowiseAgent, FlowiseAgentConfig + + +def test_flowise_agent_initialization(): + """Test FlowiseAgent initialization""" + config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="test-flow-id" + ) + + agent = FlowiseAgent(config) + + assert agent.config == config + assert agent.api_url == "http://localhost:3000/api/v1/prediction/test-flow-id" + + +def test_flowise_agent_clone(): + """Test FlowiseAgent clone method""" + config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="test-flow-id" + ) + + agent = FlowiseAgent(config) + cloned_agent = agent.clone() + + assert isinstance(cloned_agent, FlowiseAgent) + assert cloned_agent.config == config + + +@patch('ag_ui_flowise.flowise_agent.requests.post') +def test_flowise_agent_run(mock_post): + """Test FlowiseAgent run method""" + # Mock the response + mock_response = Mock() + mock_response.json.return_value = { + 'text': 'Hello from Flowise!', + 'question': 'Hello' + } + mock_response.raise_for_status.return_value = None + mock_post.return_value = mock_response + + config = FlowiseAgentConfig( + api_url="http://localhost:3000/api/v1/prediction/{flowId}", + flow_id="test-flow-id" + ) + + agent = FlowiseAgent(config) + + input_data = { + 'threadId': 'test-thread-id', + 'runId': 'test-run-id', + 'messages': [ + { + 'id': '1', + 'role': 'user', + 'content': 'Hello' + } + ] + } + + events = agent.run(input_data) + + # Check that we got the expected events + assert len(events) == 5 # RunStarted, TextMessageStart, TextMessageContent, TextMessageEnd, RunFinished, MessagesSnapshot + + # Verify the mock was called correctly + mock_post.assert_called_once() + args, kwargs = mock_post.call_args + assert args[0] == "http://localhost:3000/api/v1/prediction/test-flow-id" + assert kwargs['json']['question'] == 'Hello' \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/src/__tests__/flowise-agent.test.ts b/typescript-sdk/integrations/flowise/src/__tests__/flowise-agent.test.ts new file mode 100644 index 000000000..74e19a6eb --- /dev/null +++ b/typescript-sdk/integrations/flowise/src/__tests__/flowise-agent.test.ts @@ -0,0 +1,41 @@ +import { FlowiseAgent, FlowiseAgentConfig } from '../flowise-agent'; +import { EventType } from '@ag-ui/client'; + +describe('FlowiseAgent', () => { + it('should create a FlowiseAgent instance', () => { + const config: FlowiseAgentConfig = { + apiUrl: 'http://localhost:3000/api/v1/prediction/{flowId}', + flowId: 'test-flow-id', + }; + + const agent = new FlowiseAgent(config); + + expect(agent).toBeInstanceOf(FlowiseAgent); + expect(agent).toBeDefined(); + }); + + it('should correctly format the API URL', () => { + const config: FlowiseAgentConfig = { + apiUrl: 'http://localhost:3000/api/v1/prediction/{flowId}', + flowId: 'test-flow-id', + }; + + const agent = new FlowiseAgent(config); + + // @ts-ignore: accessing private property for testing + expect(agent.apiUrl).toBe('http://localhost:3000/api/v1/prediction/test-flow-id'); + }); + + it('should clone the agent correctly', () => { + const config: FlowiseAgentConfig = { + apiUrl: 'http://localhost:3000/api/v1/prediction/{flowId}', + flowId: 'test-flow-id', + }; + + const agent = new FlowiseAgent(config); + const clonedAgent = agent.clone(); + + expect(clonedAgent).toBeInstanceOf(FlowiseAgent); + expect(clonedAgent).not.toBe(agent); + }); +}); \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/src/__tests__/index.ts b/typescript-sdk/integrations/flowise/src/__tests__/index.ts new file mode 100644 index 000000000..bc385cd48 --- /dev/null +++ b/typescript-sdk/integrations/flowise/src/__tests__/index.ts @@ -0,0 +1 @@ +export * from './flowise-agent'; diff --git a/typescript-sdk/integrations/flowise/src/flowise-agent.ts b/typescript-sdk/integrations/flowise/src/flowise-agent.ts new file mode 100644 index 000000000..0c508e1f9 --- /dev/null +++ b/typescript-sdk/integrations/flowise/src/flowise-agent.ts @@ -0,0 +1,195 @@ +import { Observable, Subscriber } from "rxjs"; +import { + AbstractAgent, + AgentConfig, + BaseEvent, + EventType, + MessagesSnapshotEvent, + RunAgentInput, + RunFinishedEvent, + RunStartedEvent, + TextMessageContentEvent, + TextMessageEndEvent, + TextMessageStartEvent, + Message as AGUIMessage +} from "@ag-ui/client"; + +export interface FlowiseAgentConfig extends AgentConfig { + /** + * The Flowise API endpoint URL + * Example: "http://localhost:3000/api/v1/prediction/{flowId}" + */ + apiUrl: string; + + /** + * The Flowise flow ID + */ + flowId: string; + + /** + * API key for authentication (if required) + */ + apiKey?: string; + + /** + * Additional headers to send with requests + */ + headers?: Record; +} + +export interface FlowiseResponse { + text: string; + question: string; + chatId?: string; + sessionId?: string; + sourceDocuments?: Array<{ + pageContent: string; + metadata: Record; + }>; + usedTools?: Array<{ + tool: string; + toolInput: Record; + toolOutput: string; + }>; +} + +export class FlowiseAgent extends AbstractAgent { + private config: FlowiseAgentConfig; + private apiUrl: string; + + constructor(config: FlowiseAgentConfig) { + super(config); + this.config = config; + this.apiUrl = config.apiUrl.replace('{flowId}', config.flowId); + } + + public clone() { + return new FlowiseAgent(this.config); + } + + run(input: RunAgentInput): Observable { + return new Observable((subscriber) => { + this.runFlowise(input, subscriber); + return () => {}; + }); + } + + private async runFlowise(input: RunAgentInput, subscriber: Subscriber) { + try { + // Emit run started event + const runStartedEvent: RunStartedEvent = { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + }; + subscriber.next(runStartedEvent); + + // Get the last user message + const lastUserMessage = this.getLastUserMessage(input.messages); + if (!lastUserMessage) { + throw new Error("No user message found"); + } + + // Prepare the request to Flowise + const requestBody = { + question: lastUserMessage.content, + history: this.formatHistory(input.messages), + overrideConfig: { + sessionId: input.threadId, + } + }; + + // Set up headers + const headers: Record = { + 'Content-Type': 'application/json', + ...this.config.headers + }; + + if (this.config.apiKey) { + headers['Authorization'] = `Bearer ${this.config.apiKey}`; + } + + // Make the API call to Flowise + const response = await fetch(this.apiUrl, { + method: 'POST', + headers, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(`Flowise API error: ${response.status} ${response.statusText}`); + } + + const flowiseResponse: FlowiseResponse = await response.json(); + + // Emit text message events + const messageId = Date.now().toString(); + + const textMessageStartEvent: TextMessageStartEvent = { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: "assistant" + }; + subscriber.next(textMessageStartEvent); + + const textMessageContentEvent: TextMessageContentEvent = { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: flowiseResponse.text + }; + subscriber.next(textMessageContentEvent); + + const textMessageEndEvent: TextMessageEndEvent = { + type: EventType.TEXT_MESSAGE_END, + messageId + }; + subscriber.next(textMessageEndEvent); + + // Emit messages snapshot + const messagesSnapshotEvent: MessagesSnapshotEvent = { + type: EventType.MESSAGES_SNAPSHOT, + messages: [ + ...input.messages, + { + id: messageId, + role: "assistant", + content: flowiseResponse.text, + timestamp: new Date().toISOString() + } + ] + }; + subscriber.next(messagesSnapshotEvent); + + // Emit run finished event + const runFinishedEvent: RunFinishedEvent = { + type: EventType.RUN_FINISHED, + threadId: input.threadId, + runId: input.runId, + }; + subscriber.next(runFinishedEvent); + + subscriber.complete(); + } catch (error) { + subscriber.error(error); + } + } + + private getLastUserMessage(messages: AGUIMessage[]): AGUIMessage | null { + // Find the last user message by working backwards from the last message + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === "user") { + return messages[i]; + } + } + return null; + } + + private formatHistory(messages: AGUIMessage[]): Array<{role: string, content: string}> { + return messages + .filter(msg => msg.role === "user" || msg.role === "assistant") + .map(msg => ({ + role: msg.role === "user" ? "userMessage" : "apiMessage", + content: msg.content + })); + } +} \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/src/index.ts b/typescript-sdk/integrations/flowise/src/index.ts new file mode 100644 index 000000000..fbda91b0d --- /dev/null +++ b/typescript-sdk/integrations/flowise/src/index.ts @@ -0,0 +1,4 @@ +export { FlowiseAgent } from './flowise-agent'; +export type { FlowiseAgentConfig, FlowiseResponse } from './flowise-agent'; + +export * from './__tests__'; \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/tsconfig.json b/typescript-sdk/integrations/flowise/tsconfig.json new file mode 100644 index 000000000..e7ece5781 --- /dev/null +++ b/typescript-sdk/integrations/flowise/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/typescript-sdk/integrations/flowise/tsup.config.ts b/typescript-sdk/integrations/flowise/tsup.config.ts new file mode 100644 index 000000000..cba32db5f --- /dev/null +++ b/typescript-sdk/integrations/flowise/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, +}); \ No newline at end of file