├── .env.example ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── TRADEMARK ├── __tests__ ├── adapters │ └── llm-adapters │ │ ├── map-messages.test.ts │ │ └── slow │ │ ├── adapter.test.ts │ │ ├── function.test.ts │ │ └── multiple-function.test.ts └── known-issues │ └── gemini.test.ts ├── app ├── (admin) │ ├── admin.css │ ├── admin │ │ ├── [agentName] │ │ │ ├── logs │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── task-definitions │ │ │ │ ├── edit │ │ │ │ └── [[...id]] │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── agents │ │ │ └── [[...id]] │ │ │ │ └── page.tsx │ │ └── corpora │ │ │ ├── corpus │ │ │ └── [[...id]] │ │ │ │ └── page.tsx │ │ │ ├── file │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── query │ │ │ └── [id] │ │ │ └── page.tsx │ ├── api │ │ └── logs │ │ │ └── stream │ │ │ └── route.ts │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── not-found │ │ └── page.tsx │ ├── page.tsx │ ├── register │ │ └── page.tsx │ └── ui │ │ ├── agent-form.tsx │ │ ├── agent-list.tsx │ │ ├── back-button.tsx │ │ ├── collapsible-panel.tsx │ │ ├── corpora-list.tsx │ │ ├── corpus-file-form.tsx │ │ ├── corpus-form.tsx │ │ ├── corpus-query-form.tsx │ │ ├── demo-mode.tsx │ │ ├── form-field.tsx │ │ ├── lock-icon.tsx │ │ ├── login-form.tsx │ │ ├── logs-client.tsx │ │ ├── nav.tsx │ │ ├── register-form.tsx │ │ ├── selectable-card.tsx │ │ ├── sign-out-button.tsx │ │ ├── task-definition-form.tsx │ │ └── task-definition-list.tsx ├── (chat) │ ├── [agentName] │ │ ├── api │ │ │ └── ai │ │ │ │ └── route.ts │ │ └── page.tsx │ ├── chat.css │ ├── layout.tsx │ └── ui │ │ ├── chat-bot.tsx │ │ ├── message.tsx │ │ └── modal.tsx └── favicon.ico ├── auth.config.ts ├── auth.ts ├── eslint.config.mjs ├── forms ├── form-actions.ts └── payment-consent.tsx ├── jest.config.ts ├── jest.setup.ts ├── lib ├── actions │ ├── admin-actions.ts │ └── server-actions.ts ├── adapters │ ├── embedding-adapters │ │ └── openai-embedding-adapter.ts │ └── llm-adapters │ │ ├── anthropic-llm-adapter.ts │ │ ├── google-llm-adapter.ts │ │ ├── llm-model-factory.ts │ │ ├── openai-llm-adapter.ts │ │ └── system-adapters.ts ├── agent.ts ├── build-tools.ts ├── corpus-file.ts ├── corpus-query.ts ├── corpus.ts ├── db │ ├── db-admin-service.ts │ ├── db-service.ts │ └── prisma.ts ├── logger.ts ├── register-user.ts ├── session-storage.ts ├── system-tools │ ├── system-client-tools.ts │ ├── system-context-tools.ts │ └── system-server-tools.ts ├── task-definition.ts ├── task-machine.ts ├── user.ts ├── utils │ └── cn.ts └── validation.ts ├── middleware.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma └── schema.prisma ├── public ├── superexpert-ai-black-transparent.png ├── superexpert-ai-transparent.png └── themes │ ├── blue │ └── blue-preview.png │ ├── default │ └── default-preview.png │ └── modern │ ├── modern-preview.png │ └── white-carbon.webp ├── rag-strategies ├── keyword-strategy.ts ├── rag-strategies.ts └── semantic-strategy.ts ├── scripts └── trim-demo-agents.ts ├── superexpert-ai.config.ts ├── superexpert-ai.plugins.client.ts ├── superexpert-ai.plugins.server.ts ├── tailwind.config.ts ├── themes └── chat-bot │ ├── blue.module.css │ ├── default-preview.png │ ├── default-preview.ts │ ├── default.module.css │ ├── modern.module.css │ └── themes.ts ├── tools ├── client-tools.ts ├── context-tools.ts └── server-tools.ts ├── tsconfig.json └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | # PostgreSQL Database Connection (replace username, password, host, port, and dbname) 2 | DATABASE_URL='postgresql://username:password@localhost:5432/superexpert_ai_db' 3 | 4 | # LLM Provider API Keys (register accounts to obtain your keys) 5 | OPENAI_API_KEY='' 6 | GEMINI_API_KEY='' 7 | ANTHROPIC_API_KEY='' 8 | 9 | # NextAuth.js v5 Authentication 10 | NEXTAUTH_URL='http://localhost:3000' 11 | NEXTAUTH_SECRET='' # Generate with openssl rand -base64 32 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSameLine": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Superexpert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Source AI Made Simple 2 | 3 | **Superexpert.AI**: Build advanced, multi-task AI Agents—no coding required. 4 | 5 | ## Overview 6 | 7 | Superexpert.AI is an open-source platform designed to simplify the creation and management of sophisticated AI agents. With an intuitive interface and powerful features, users can build, deploy, and monitor AI solutions without writing a single line of code. 8 | 9 | ## Getting Started 10 | 11 | Ready to dive in? Follow our [Install in Minutes](https://superexpert.ai/docs/install-in-minutes/) guide to set up Superexpert.AI quickly. Once installed, our [Quick Start](https://superexpert.ai/docs/quick-start/) tutorial will walk you through creating your first AI agent. 12 | 13 | For comprehensive details on all features and capabilities, refer to our [Full Documentation](https://superexpert.ai/docs/). 14 | 15 | ## Community and Support 16 | 17 | Join our community to share your experiences, request features, and report issues: 18 | 19 | - **Feature Requests & Discussions**: Engage with fellow users and suggest new features on our [GitHub Discussions](https://github.com/Superexpert/superexpert-ai/discussions) page. 20 | 21 | - **Bug Reports**: If you encounter any issues, please let us know through our [GitHub Issue Tracker](https://github.com/Superexpert/superexpert-ai/issues). 22 | 23 | We encourage you to download Superexpert.AI today and start building innovative AI agents. Your participation helps us improve and shape the future of the platform. -------------------------------------------------------------------------------- /TRADEMARK: -------------------------------------------------------------------------------- 1 | Superexpert Trademark Policy 2 | 3 | 1. Introduction and Purpose 4 | 5 | This Trademark Policy governs the use of the trademarks owned by Superexpert, including the company name “Superexpert” and the product name “Superexpert AI” (collectively, the “Trademarks”). The purpose of this policy is to protect the integrity and reputation of our brand while clarifying the guidelines for permitted and prohibited uses of the Trademarks. This document is provided for informational purposes only and does not constitute legal advice. Please consult with legal counsel for a detailed interpretation. 6 | 7 | 2. Definitions 8 | • Trademarks: 9 | “Superexpert” and “Superexpert AI,” including any associated logos, stylized text, or graphic representations, are considered the Trademarks covered by this policy. 10 | • Authorized Use: 11 | Refers to any use of the Trademarks that is expressly permitted under this policy or with prior written consent from Superexpert. 12 | • Unauthorized Use: 13 | Refers to any use of the Trademarks that violates this policy, including any use that misleads or confuses consumers regarding the origin, endorsement, or affiliation of a product or service. 14 | 15 | 3. List of Protected Marks 16 | 17 | The following are the official Trademarks protected under this policy: 18 | • Superexpert (Company Name) 19 | • Superexpert AI (Product Name) 20 | 21 | Any logos or graphic representations provided by Superexpert are also protected and must be used in accordance with this policy. 22 | 23 | 4. Permitted Use 24 | • Reference and Attribution: 25 | Users may refer to Superexpert and Superexpert AI in a descriptive manner when discussing the official framework. Proper attribution must be given, and references should accurately reflect the origin of the Trademarks. 26 | • Non-Endorsed Use: 27 | When using the Trademarks, third parties must include a disclaimer clarifying that their use of the name does not imply any endorsement, sponsorship, or affiliation with Superexpert unless explicitly authorized in writing. 28 | • Linking Requirements: 29 | When referencing the official framework in online or printed materials, users are encouraged to include a link to the official Superexpert website or indicate that the framework is developed by Superexpert. 30 | 31 | 5. Prohibited Use 32 | • Rebranding and Renaming: 33 | Although the underlying code of Superexpert AI is open source under a permissive license, no party is permitted to rebrand, rename, or redistribute the framework under the names “Superexpert” or “Superexpert AI,” or any name that is confusingly similar, without prior written consent from Superexpert. 34 | • Modification of the Trademarks: 35 | The Trademarks must be used in their entirety. No alterations, distortions, or modifications are allowed that would dilute or misrepresent the value and distinctiveness of the brand. 36 | • Implying Endorsement: 37 | Any use of the Trademarks that suggests sponsorship, endorsement, or an official affiliation with Superexpert beyond the factual description of the framework is strictly prohibited. 38 | 39 | 6. Enforcement and Remedies 40 | 41 | Superexpert reserves all rights to the Trademarks. Unauthorized use may result in legal action to enforce these trademark rights. In cases of infringement or misuse, Superexpert may: 42 | • Issue a cease and desist notice. 43 | • Pursue legal remedies available under applicable law. 44 | 45 | Users who become aware of potential infringements are encouraged to notify Superexpert at the contact information provided below. 46 | 47 | 7. Contact Information 48 | 49 | For inquiries, permissions, or to report potential trademark infringements, please contact: 50 | 51 | Email: legal@superexpert.com 52 | 53 | 8. Updates and Revisions 54 | 55 | This Trademark Policy is subject to revision and updates at the discretion of Superexpert. Users are advised to periodically review this document for any changes. Continued use of the Trademarks following any such modifications will be deemed acceptance of those changes. -------------------------------------------------------------------------------- /__tests__/adapters/llm-adapters/map-messages.test.ts: -------------------------------------------------------------------------------- 1 | import { LLMModelFactory } from '@/lib/adapters/llm-adapters/llm-model-factory'; 2 | import { MessageAI } from '@superexpert-ai/framework'; 3 | import '@/lib/adapters/llm-adapters/system-adapters'; 4 | 5 | describe('Adapter Map Messages', () => { 6 | it('should map Google messages to Content', async () => { 7 | // Arrange 8 | const messages: MessageAI[] = [ 9 | { 10 | role: 'user', 11 | content: 'What is the capital of France?', 12 | }, 13 | ]; 14 | // Act 15 | const modelId = 'gemini-2.0-flash'; 16 | const adapter = LLMModelFactory.createModel(modelId, { 17 | temperature: 0.7, 18 | maximumOutputTokens: 2048, 19 | }); 20 | const results = adapter.mapMessages(messages); 21 | console.dir(results, { depth: null, colors: true }); 22 | 23 | // Assert 24 | expect(results).toEqual([ 25 | { 26 | role: 'user', 27 | parts: [{ text: 'What is the capital of France?' }], 28 | }, 29 | ]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/adapters/llm-adapters/slow/adapter.test.ts: -------------------------------------------------------------------------------- 1 | import 'openai/shims/node'; 2 | import { LLMModelFactory } from '@/lib/adapters/llm-adapters/llm-model-factory'; 3 | import { MessageAI } from '@superexpert-ai/framework'; 4 | import { ToolAI } from '@superexpert-ai/framework'; 5 | import { getLLMDefinitions } from '@superexpert-ai/framework'; 6 | import '@/lib/adapters/llm-adapters/system-adapters'; 7 | 8 | 9 | /*********** 10 | * 11 | * This test suite is for testing the LLM adapters of the models. 12 | * It verifies that the models can generate responses correctly 13 | * when prompted with specific instructions and input messages. 14 | * 15 | * The test cases iterate through all available models and check their responses. 16 | * 17 | * The expected response is that the model should mention "George Washington" 18 | * when asked about the first president of the United States. 19 | * 20 | * The test is set to timeout after 60 seconds because this is a long-running tests. 21 | */ 22 | 23 | const models = getLLMDefinitions(); 24 | const testCases = models.map((model) => [model.id, model.name]); 25 | 26 | describe('Adapter tests', () => { 27 | test.each(testCases)( 28 | 'should work with model: %s (%s)', 29 | async (modelId, modelName) => { 30 | // Arrange 31 | const adapter = LLMModelFactory.createModel(modelId); 32 | const instructions = 'You are a helpful assistant.'; 33 | const inputMessages: MessageAI[] = [ 34 | { 35 | role: 'user', 36 | content: 37 | 'Who was the first president of the United States?', 38 | }, 39 | ]; 40 | const tools: ToolAI[] = []; 41 | const options = {}; 42 | 43 | // Act 44 | let results = ''; 45 | const generator = adapter.generateResponse( 46 | instructions, 47 | inputMessages, 48 | tools, 49 | options 50 | ); 51 | for await (const result of generator) { 52 | results += result.text; 53 | } 54 | 55 | // Assert 56 | expect(results).toContain('George Washington'); 57 | }, 58 | 1000 * 60 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /__tests__/adapters/llm-adapters/slow/function.test.ts: -------------------------------------------------------------------------------- 1 | import { LLMModelFactory } from '@/lib/adapters/llm-adapters/llm-model-factory'; 2 | import { MessageAI} from '@superexpert-ai/framework'; 3 | import { ToolAI } from '@superexpert-ai/framework'; 4 | import { getLLMDefinitions } from '@superexpert-ai/framework'; 5 | import '@/lib/adapters/llm-adapters/system-adapters'; 6 | 7 | 8 | /*********** 9 | * 10 | * This test suite is for testing the function adapter of the LLM models. 11 | * It verifies that the models can correctly call a function (getWeather) 12 | * when prompted with a specific instruction. 13 | */ 14 | 15 | const models = getLLMDefinitions(); 16 | 17 | const testCases = models.map(model => [model.id, model.name]); 18 | 19 | const tools: ToolAI[] = [ 20 | { 21 | type: 'function', 22 | function: { 23 | name: 'getWeather', 24 | description: 'This is a tool to get the weather', 25 | parameters: { 26 | type: 'object', 27 | properties: { 28 | location: { type: 'string', description: 'This is the location' }, 29 | unit: { 30 | type: 'string', 31 | description: 'Return the temperature in Celcius or Fahrenheit' 32 | } 33 | }, 34 | required: [ 'location', 'unit' ] 35 | } 36 | } 37 | } 38 | ]; 39 | 40 | 41 | 42 | describe('Adapter Function Tests', () => { 43 | test.each(testCases)( 44 | 'should work with model: %s (%s)', 45 | async (modelId, modelName) => { 46 | // Arrange 47 | const adapter = LLMModelFactory.createModel(modelId); 48 | const instructions = 'You are a helpful assistant. Call the getWeather function when asked about the weather.'; 49 | const inputMessages: MessageAI[] = [{ 50 | role: 'user', 51 | content: 'What is the weather in San Francisco in Fahrenheit?' 52 | }]; 53 | const options = {}; 54 | 55 | // Act 56 | let results = []; 57 | const generator = adapter.generateResponse(instructions, inputMessages, tools, options); 58 | for await (const result of generator) { 59 | results.push(result); 60 | } 61 | 62 | // Parse the arguments JSON strings into objects - otherwise, the 63 | // order of the parameters in the JSON string can cause the test to fail 64 | const parsedResults = results 65 | .filter(item => item.toolCall) // Filter out non-toolCall items 66 | .map(item => ({ 67 | ...item, 68 | toolCall: { 69 | ...item.toolCall, 70 | function: { 71 | ...item.toolCall?.function, 72 | arguments: JSON.parse(item.toolCall?.function.arguments), 73 | }, 74 | }, 75 | })); 76 | 77 | 78 | // Assert 79 | expect(parsedResults).toEqual( 80 | expect.arrayContaining([ 81 | expect.objectContaining({ 82 | toolCall: expect.objectContaining({ 83 | function: expect.objectContaining({ 84 | arguments: { 85 | location: 'San Francisco', 86 | unit: 'Fahrenheit', 87 | }, 88 | name: 'getWeather', 89 | }), 90 | id: expect.any(String), // Ensures 'id' is a string 91 | type: 'function', 92 | }), 93 | }), 94 | ]) 95 | ); 96 | 97 | }, 1000 * 60 98 | ); 99 | 100 | }); 101 | -------------------------------------------------------------------------------- /__tests__/adapters/llm-adapters/slow/multiple-function.test.ts: -------------------------------------------------------------------------------- 1 | import 'openai/shims/node'; 2 | import { LLMModelFactory } from '@/lib/adapters/llm-adapters/llm-model-factory'; 3 | import { MessageAI} from '@superexpert-ai/framework'; 4 | import { ToolAI } from '@superexpert-ai/framework'; 5 | import { getLLMDefinitions } from '@superexpert-ai/framework'; 6 | import '@/lib/adapters/llm-adapters/system-adapters'; 7 | 8 | 9 | /*********** 10 | * 11 | * This test suite is for testing multiple function calls for each LLM model. 12 | * It verifies that the models can correctly call multiple functions (getWeather and getMovies) 13 | * when prompted with a specific instruction. 14 | * 15 | * Note: Sometimes Claude Opus will just think and not act and fail the test. 16 | */ 17 | 18 | const models = getLLMDefinitions() 19 | .filter(model => model.id !== 'claude-3-7-sonnet-20250219') // Claude 3.7 Sonnet will work with thinking enabled 20 | .filter(model => model.provider !== 'google'); // Google models do not support parallel function calls 21 | 22 | 23 | const testCases = models.map(model => [model.id, model.name]); 24 | const tools:ToolAI[] = [ 25 | { 26 | type: 'function', 27 | function: { 28 | name: 'getWeather', 29 | description: 'This is a tool to get the weather', 30 | parameters: { 31 | type: 'object', 32 | properties: { 33 | location: { type: 'string', description: 'This is the location' }, 34 | unit: { 35 | type: 'string', 36 | description: 'Return the temperature in Celcius or Fahrenheit', 37 | enum: [ 'Celsius', 'Fahrenheit' ] 38 | } 39 | }, 40 | required: [ 'location', 'unit' ] 41 | } 42 | } 43 | }, 44 | { 45 | type: 'function', 46 | function: { 47 | name: 'getMovies', 48 | description: 'This is a tool to get a list of available movies in a location', 49 | parameters: { 50 | type: 'object', 51 | properties: { 52 | location: { type: 'string', description: 'This is the location' }, 53 | }, 54 | required: [ 'location' ] 55 | } 56 | } 57 | } 58 | ]; 59 | 60 | 61 | 62 | describe('Adapter Multiple Function Tests', () => { 63 | test.each(testCases)( 64 | 'should work with model: %s (%s)', 65 | async (modelId, modelName) => { 66 | // Arrange 67 | const adapter = LLMModelFactory.createModel(modelId); 68 | const instructions = 'You are a helpful assistant.'; 69 | const inputMessages:MessageAI[] = [{ 70 | role: 'user', 71 | //content: 'What is the current weather in San Francisco in Fahrenheit?' 72 | //content: "Please provide two pieces of information: 1) the current weather in San Francisco in Fahrenheit, and 2) a list of movies currently being shown in San Francisco. Use the appropriate tools for each request." 73 | //content: "Call both the getMovies and getWeather functions for San Francisco at the same time. Do not call these functions in sequence. For getWeather, use Fahrenheit as the unit." 74 | content: "Please retrieve both the current weather (in Fahrenheit) and the list of movies playing in San Francisco. These function calls should be executed in parallel." 75 | }]; 76 | const options = {}; 77 | 78 | // Act 79 | let results = []; 80 | const generator = adapter.generateResponse(instructions, inputMessages, tools, options); 81 | for await (const result of generator) { 82 | results.push(result); 83 | } 84 | 85 | // Parse the arguments JSON strings into objects - otherwise, the 86 | // order of the parameters in the JSON string can cause the test to fail 87 | const parsedResults = results 88 | .filter(item => item.toolCall) // Filter out non-toolCall items 89 | .map(item => ({ 90 | ...item, 91 | toolCall: { 92 | ...item.toolCall, 93 | function: { 94 | ...item.toolCall?.function, 95 | arguments: JSON.parse(item.toolCall?.function.arguments), 96 | }, 97 | }, 98 | })); 99 | 100 | 101 | // Assert 102 | expect(parsedResults).toEqual( 103 | expect.arrayContaining([ 104 | expect.objectContaining({ 105 | toolCall: expect.objectContaining({ 106 | function: expect.objectContaining({ 107 | arguments: { 108 | location: 'San Francisco', 109 | unit: 'Fahrenheit', 110 | }, 111 | name: 'getWeather', 112 | }), 113 | id: expect.any(String), // Ensures 'id' is a string 114 | type: 'function', 115 | }), 116 | }), 117 | expect.objectContaining({ 118 | toolCall: expect.objectContaining({ 119 | function: expect.objectContaining({ 120 | arguments: { 121 | location: 'San Francisco', 122 | }, 123 | name: 'getMovies', 124 | }), 125 | id: expect.any(String), // Ensures 'id' is a string 126 | type: 'function', 127 | }), 128 | }), 129 | ]) 130 | ); 131 | 132 | }, 1000 * 60 133 | ); 134 | 135 | }); 136 | -------------------------------------------------------------------------------- /__tests__/known-issues/gemini.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageAI} from '@superexpert-ai/framework'; 2 | import { ToolAI } from '@superexpert-ai/framework'; 3 | import { LLMModelFactory } from '@/lib/adapters/llm-adapters/llm-model-factory'; 4 | import '@/lib/adapters/llm-adapters/system-adapters'; 5 | 6 | 7 | /*********** 8 | * Known Issue: Gemini 2.0 Flash will return MALFORMED_FUNCTION_CALL 9 | * when asked to do multiple function but this issue does not impact 10 | * Gemini 2.0 Pro. 11 | * 12 | * The best workaround for this issue is to use prompt engineering 13 | * to tell Gemini to only call one function at a time. (in the Instructions) 14 | */ 15 | 16 | 17 | 18 | const tools: ToolAI[] = [ 19 | { 20 | type: 'function', 21 | function: { 22 | name: 'getWeather', 23 | description: 'This is a tool to get the weather', 24 | parameters: { 25 | type: 'object', 26 | properties: { 27 | location: { type: 'string', description: 'This is the location' }, 28 | unit: { 29 | type: 'string', 30 | description: 'Return the temperature in Celsius or Fahrenheit' 31 | } 32 | }, 33 | required: [ 'location', 'unit' ] 34 | } 35 | } 36 | } 37 | ]; 38 | 39 | const inputMessages: MessageAI[] = [ 40 | { role: 'user', content: 'Hello' }, 41 | { 42 | role: 'assistant', 43 | content: 'Hello! How can I help you today?\n', 44 | }, 45 | { 46 | role: 'user', 47 | content: 48 | 'What is the weather in Paris, France and Austin and San Francisco in Celsius?', 49 | }, 50 | { 51 | role: 'assistant', 52 | content: '...', 53 | tool_calls: [ 54 | { 55 | id: 'getWeather', 56 | type: 'function', 57 | function: { 58 | name: 'getWeather', 59 | arguments: 60 | '{"unit":"Celsius","location":"Paris, France"}', 61 | }, 62 | }, 63 | { 64 | id: 'getWeather', 65 | type: 'function', 66 | function: { 67 | name: 'getWeather', 68 | arguments: '{"location":"Austin","unit":"Celsius"}', 69 | }, 70 | }, 71 | { 72 | id: 'getWeather', 73 | type: 'function', 74 | function: { 75 | name: 'getWeather', 76 | arguments: 77 | '{"location":"San Francisco","unit":"Celsius"}', 78 | }, 79 | }, 80 | ], 81 | }, 82 | { 83 | role: 'tool', 84 | content: 'The weather in Paris, France is unimaginably awful', 85 | tool_call_id: 'getWeather', 86 | }, 87 | { 88 | role: 'tool', 89 | content: 'The weather in Austin in Celsius is hot.', 90 | tool_call_id: 'getWeather', 91 | }, 92 | { 93 | role: 'tool', 94 | content: 'The weather in San Francisco in Celsius is foggy.', 95 | tool_call_id: 'getWeather', 96 | }, 97 | { 98 | role: 'assistant', 99 | content: 100 | 'The weather in Paris, France is unimaginably awful. The weather in Austin in Celsius is hot. The weather in San Francisco in Celsius is foggy.\n', 101 | }, 102 | { 103 | role: 'user', 104 | content: 105 | 'What is the weather in San Francisco, Modesto, and LA?', 106 | }, 107 | { 108 | role: 'assistant', 109 | content: 110 | 'What units would you like the temperature in? Celsius or Fahrenheit?\n', 111 | }, 112 | { role: 'user', content: 'Celsius' }, 113 | ]; 114 | 115 | describe('Complex function calling ', () => { 116 | 117 | 118 | it('with Gemini 2.0 Pro works correctly', async () => { 119 | 120 | // Arrange 121 | const adapter = LLMModelFactory.createModel('gemini-2.0-pro-exp-02-05'); 122 | 123 | // Act 124 | const generator = adapter.generateResponse( 125 | 'You are a helpful assistant.', 126 | inputMessages, 127 | tools, 128 | {} 129 | ); 130 | let results = []; 131 | for await (const result of generator) { 132 | results.push(result); 133 | } 134 | //console.dir(results, { depth: null }); 135 | }), 136 | 137 | it('with Gemini 2.0 Flash results in MALFORMED_FUNCTION_CALL', async () => { 138 | // Act 139 | const adapter = LLMModelFactory.createModel('gemini-2.0-flash'); 140 | 141 | // Correctly test for async rejections using await expect().rejects 142 | await expect(async () => { 143 | const generator = adapter.generateResponse( 144 | 'You are a helpful assistant.', 145 | inputMessages, 146 | tools, 147 | {} 148 | ); 149 | 150 | let results = []; 151 | for await (const result of generator) { 152 | results.push(result); 153 | } 154 | }).rejects.toThrow('MALFORMED_FUNCTION_CALL') 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /app/(admin)/admin.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-gray-50; 7 | } 8 | 9 | p { 10 | @apply text-base text-gray-700; 11 | } 12 | 13 | a { 14 | @apply text-blue-600 hover:underline; 15 | } 16 | 17 | @layer components { 18 | .btnPrimary { 19 | @apply inline-flex 20 | items-center 21 | justify-center 22 | rounded-full 23 | bg-orange-500 24 | hover:bg-orange-600 25 | px-4 py-2 26 | sm:px-5 27 | sm:py-2.5 text-sm sm:text-base font-semibold text-white shadow transition duration-200 hover:no-underline; 28 | } 29 | 30 | .btnSecondary { 31 | @apply bg-white inline-flex items-center px-4 py-2 border border-gray-300 text-gray-600 font-medium rounded-full shadow-sm hover:bg-gray-100 hover:no-underline transition; 32 | } 33 | 34 | .btnDanger { 35 | @apply inline-flex items-center justify-center rounded-full 36 | bg-red-500 hover:bg-red-600 37 | px-4 py-2 sm:px-5 sm:py-2.5 38 | text-sm sm:text-base font-semibold text-white 39 | shadow transition duration-200 40 | hover:no-underline 41 | 42 | /* ── disabled state —————————————————————— */ 43 | disabled:bg-red-300 /* lighter shade */ 44 | disabled:text-white/60 /* fade text a bit */ 45 | disabled:cursor-not-allowed 46 | disabled:hover:bg-red-300 /* keep same colour on hover */ 47 | disabled:opacity-60; 48 | } 49 | 50 | .pageHeader { 51 | @apply text-3xl font-bold text-gray-900; 52 | } 53 | 54 | .pageContainer { 55 | @apply max-w-4xl 56 | mx-auto 57 | px-4 58 | py-8 59 | sm:px-6; 60 | } 61 | 62 | .pageCard { 63 | @apply bg-white 64 | rounded-2xl 65 | shadow-md 66 | p-8 67 | space-y-6; 68 | } 69 | 70 | .formField { 71 | @apply mb-6 72 | flex 73 | flex-col; 74 | } 75 | 76 | .fieldInstructions { 77 | @apply text-zinc-500 78 | text-sm 79 | font-normal 80 | leading-snug 81 | mt-0.5; 82 | } 83 | 84 | fieldset { 85 | @apply space-y-2; 86 | } 87 | 88 | label { 89 | @apply text-neutral-800 90 | text-base 91 | font-bold 92 | leading-snug; 93 | } 94 | 95 | /* Input fields */ 96 | input, 97 | textarea { 98 | @apply mt-1 99 | mb-1 100 | w-full border 101 | border-neutral-300 102 | rounded-lg 103 | px-4 104 | py-3 105 | text-base 106 | focus:outline-none 107 | focus:ring-2 108 | focus:ring-orange-500; 109 | } 110 | 111 | /* Textarea */ 112 | textarea { 113 | @apply resize-none 114 | leading-relaxed 115 | max-h-40 116 | overflow-y-auto; 117 | } 118 | 119 | input[readonly], 120 | textarea[readonly] { 121 | @apply bg-gray-100 text-gray-500 border border-gray-200 pr-10 cursor-not-allowed; 122 | } 123 | 124 | input[type='checkbox'] { 125 | @apply h-4 126 | w-4 127 | text-orange-500 128 | border-gray-300 129 | rounded 130 | focus:ring-orange-500 131 | transition; 132 | } 133 | 134 | .error { 135 | @apply text-red-500 136 | text-sm 137 | mt-1 138 | leading-tight; 139 | } 140 | 141 | .demoMode { 142 | @apply max-w-5xl w-full bg-yellow-50 border border-yellow-200 text-yellow-900 text-base rounded-xl px-6 py-4 mt-6 mb-4 shadow-sm text-center; 143 | } 144 | 145 | .collapsiblePanel { 146 | border: 1px solid #ccc; 147 | margin-bottom: 10px; 148 | } 149 | 150 | .collapsibleHeader { 151 | width: 100%; 152 | text-align: left; 153 | padding: 10px; 154 | background-color: #f1f1f1; 155 | border: none; 156 | cursor: pointer; 157 | } 158 | 159 | .collapsibleContent { 160 | overflow: hidden; 161 | transition: height 0.6s ease; 162 | } 163 | 164 | .collapsibleContentInner { 165 | padding: 10px; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/(admin)/admin/[agentName]/logs/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import LogsClient from '@/app/(admin)/ui/logs-client'; 3 | 4 | 5 | export default async function LogsPage({ 6 | params, 7 | }: { 8 | params: Promise<{ [key: string]: string }>; 9 | }) { 10 | // Check for valid agent name 11 | // No point in authorizing here, we perform authorization in the route 12 | const resolvedParams = await params; 13 | const {agentName } = resolvedParams; 14 | return ( 15 |
16 |
17 | 20 | }> 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/(admin)/admin/[agentName]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Agent } from '@/lib/agent'; 3 | import AgentForm from '@/app/(admin)/ui/agent-form'; 4 | import { getAgentByIdAction } from '@/lib/actions/admin-actions'; 5 | 6 | export default async function EditAgentPage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | const { id } = await params; 12 | const agentId = id && id.length === 1 ? id[0] : undefined; 13 | 14 | const isEditMode = Boolean(id); 15 | 16 | let agent: Agent = { 17 | name: '', 18 | description: '', 19 | }; 20 | 21 | if (isEditMode) { 22 | agent = await getAgentByIdAction(agentId as string); 23 | } 24 | 25 | return ( 26 |
27 |
28 | 31 | }> 32 | 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/(admin)/admin/[agentName]/task-definitions/edit/[[...id]]/page.tsx: -------------------------------------------------------------------------------- 1 | import TaskDefinitionForm from '@/app/(admin)/ui/task-definition-form'; 2 | import { Suspense } from 'react'; 3 | import { getAgentAction } from '@/lib/actions/server-actions'; 4 | import { 5 | getTaskDefinitionFormDataAction, 6 | getTaskDefinitionByIdAction, 7 | } from '@/lib/actions/admin-actions'; 8 | import { TaskDefinition } from '@/lib/task-definition'; 9 | 10 | export default async function EditTaskDefinitionPage({ 11 | params, 12 | }: { 13 | params: Promise<{ [key: string]: string }>; 14 | }) { 15 | // Check for valid agent name 16 | const resolvedParams = await params; 17 | const agent = await getAgentAction(resolvedParams); 18 | 19 | const { id } = await params; 20 | const taskId = id && id.length === 1 ? id[0] : undefined; 21 | 22 | const isEditMode = Boolean(id); 23 | 24 | const { attachments, corpora, contextTools, ragStrategies, serverTools, clientTools, llmModels } = 25 | await getTaskDefinitionFormDataAction(taskId); 26 | 27 | let taskDefinition: TaskDefinition = { 28 | agentId: agent.id, 29 | isSystem: false, 30 | name: '', 31 | description: '', 32 | instructions: '', 33 | startNewThread: false, 34 | corpusLimit: 3, 35 | corpusSimilarityThreshold: 50, 36 | corpusIds: [], 37 | contextToolIds: [], 38 | ragStrategyId: 'semantic', 39 | serverToolIds: [], 40 | clientToolIds: [], 41 | modelId: 'global', 42 | maximumOutputTokens: null, 43 | temperature: null, 44 | theme: 'global', 45 | }; 46 | 47 | if (isEditMode) { 48 | taskDefinition = await getTaskDefinitionByIdAction(taskId!); 49 | } 50 | 51 | return ( 52 |
53 | 56 | }> 57 | 70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/(admin)/admin/[agentName]/task-definitions/page.tsx: -------------------------------------------------------------------------------- 1 | import TaskDefinitionList from '@/app/(admin)/ui/task-definition-list'; 2 | import { getAgentAction } from '@/lib/actions/server-actions'; 3 | import { Suspense } from 'react'; 4 | import { getTaskDefinitionListAction } from '@/lib/actions/admin-actions'; 5 | 6 | export default async function TaskListPage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | // Check for valid agent name 12 | const resolvedParams = await params; 13 | const agent = await getAgentAction(resolvedParams); 14 | 15 | const taskDefinitions = await getTaskDefinitionListAction(agent.id); 16 | return ( 17 |
18 | 21 | }> 22 | 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/(admin)/admin/agents/[[...id]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Agent } from '@/lib/agent'; 3 | import AgentForm from '@/app/(admin)/ui/agent-form'; 4 | import { getAgentByIdAction } from '@/lib/actions/admin-actions'; 5 | 6 | export default async function EditAgentPage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | const { id } = await params; 12 | const agentId = id && id.length === 1 ? id[0] : undefined; 13 | 14 | const isEditMode = Boolean(id); 15 | 16 | let agent: Agent = { 17 | name: '', 18 | description: '', 19 | }; 20 | 21 | if (isEditMode) { 22 | agent = await getAgentByIdAction(agentId as string); 23 | } 24 | 25 | return ( 26 |
27 |
28 | 31 | }> 32 | 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/(admin)/admin/corpora/corpus/[[...id]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import CorpusForm from '@/app/(admin)/ui/corpus-form'; 3 | import { getCorpusByIdAction } from '@/lib/actions/admin-actions'; 4 | import { Corpus } from '@/lib/corpus'; 5 | 6 | export default async function EditCorpusPage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | const { id } = await params; 12 | const corpusId = id && id.length === 1 ? id[0] : undefined; 13 | const isEditMode = Boolean(corpusId); 14 | 15 | let corpus: Corpus = { 16 | name: '', 17 | description: '', 18 | corpusFiles: [], 19 | }; 20 | 21 | if (isEditMode) { 22 | corpus = await getCorpusByIdAction(corpusId!); 23 | } 24 | 25 | return ( 26 |
27 |
28 | 31 | }> 32 | 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/(admin)/admin/corpora/file/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import CorpusFileForm from '@/app/(admin)/ui/corpus-file-form'; 3 | import { getCorpusByIdAction } from '@/lib/actions/admin-actions'; 4 | 5 | 6 | export default async function CorpusFilePage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | const { id } = await params; 12 | const corpus = await getCorpusByIdAction(id); 13 | 14 | 15 | return ( 16 |
17 |
18 | 21 | }> 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/(admin)/admin/corpora/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import CorporaList from '@/app/(admin)/ui/corpora-list'; 3 | import { getCorporaListAction } from '@/lib/actions/admin-actions'; 4 | import { redirect } from 'next/navigation'; 5 | 6 | export default async function WisdomListPage() { 7 | const corpora = await getCorporaListAction(); 8 | // If the uer does not have any corpora, redirect to the corpora page 9 | if (corpora.length === 0) { 10 | redirect('/admin/corpora/corpus'); 11 | } 12 | 13 | return ( 14 |
15 |
16 | 19 | }> 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/(admin)/admin/corpora/query/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import CorpusQueryForm from '@/app/(admin)/ui/corpus-query-form'; 3 | import { getCorpusByIdAction } from '@/lib/actions/admin-actions'; 4 | import { getRAGStrategiesList } from '@superexpert-ai/framework'; 5 | 6 | export default async function EditCorpusPage({ 7 | params, 8 | }: { 9 | params: Promise<{ [key: string]: string }>; 10 | }) { 11 | const { id } = await params; 12 | const corpus = await getCorpusByIdAction(id); 13 | const ragStrategies = getRAGStrategiesList(); 14 | 15 | 16 | return ( 17 |
18 |
19 | 22 | }> 23 | 24 | 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/(admin)/api/logs/stream/route.ts: -------------------------------------------------------------------------------- 1 | 2 | export const runtime = 'nodejs'; 3 | export const maxDuration = 300; // 5-minute window on Vercel 4 | 5 | import { auth } from '@/auth'; 6 | import { NextRequest, NextResponse } from 'next/server'; 7 | import { DBAdminService } from '@/lib/db/db-admin-service'; 8 | import { User } from '@superexpert-ai/framework'; 9 | 10 | 11 | export async function GET(req: NextRequest) { 12 | const session = await auth(); 13 | if (!session || !session.user) { 14 | throw new Error('User not authenticated'); 15 | } 16 | const user = session.user as User; 17 | if (!user) return NextResponse.json([], { status: 401 }); 18 | 19 | const { searchParams } = new URL(req.url); 20 | const agentName = searchParams.get('agentName'); 21 | if (!agentName) { 22 | return NextResponse.json([], { status: 400 }); 23 | } 24 | 25 | // Need to get agentId from agentName + userId (this is for authorization) 26 | const db = new DBAdminService(user.id); 27 | const agent = await db.getAgentByName(agentName); 28 | if (!agent) { 29 | return NextResponse.json([], { status: 404 }); 30 | } 31 | 32 | 33 | 34 | let lastId = BigInt(0); // remember progress per connection 35 | 36 | const stream = new ReadableStream({ 37 | async start(controller) { 38 | // poll loop 39 | const tick = async () => { 40 | const rows = await db.getLogStream( 41 | lastId, 42 | agent.id, 43 | 100, // max rows per poll 44 | ); 45 | 46 | for (const r of rows) { 47 | lastId = r.id; 48 | 49 | // guard: turn non-objects into an empty object 50 | const extras = 51 | r.data && typeof r.data === 'object' && !Array.isArray(r.data) 52 | ? (r.data as Record) 53 | : {}; 54 | 55 | controller.enqueue( 56 | `data:${JSON.stringify({ 57 | time: r.createdAt.getTime(), 58 | level: r.level, 59 | msg: r.msg, 60 | userId: r.userId, 61 | agentId: r.agentId, 62 | component: r.component, 63 | ...extras, // safe spread 64 | })}\n\n`, 65 | ); 66 | } 67 | // heartbeat (keeps idle < 25 s) 68 | controller.enqueue(':\n\n'); 69 | timer = setTimeout(tick, 1000); // 1-s server-side poll 70 | }; 71 | 72 | let timer = setTimeout(tick, 0); 73 | 74 | req.signal.addEventListener('abort', () => { 75 | clearTimeout(timer); 76 | controller.close(); 77 | }); 78 | }, 79 | }); 80 | 81 | return new Response(stream, { 82 | headers: { 83 | 'Content-Type': 'text/event-stream', 84 | 'Cache-Control': 'no-cache', 85 | Connection: 'keep-alive', 86 | }, 87 | }); 88 | } 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Plus_Jakarta_Sans } from 'next/font/google'; 3 | import '@/app/(admin)/admin.css'; 4 | import Nav from './ui/nav'; 5 | import '@/superexpert-ai.plugins.server'; 6 | 7 | const plusJakartaSans = Plus_Jakarta_Sans({ 8 | subsets: ['latin'], 9 | weight: ['400', '500', '700'], // Adjust weights as needed 10 | variable: '--font-plus-jakarta-sans', 11 | display: 'swap', 12 | }); 13 | 14 | export const metadata: Metadata = { 15 | title: 'Superexpert AI - Open Source AI Made Simple', 16 | description: 'Build powerful AI agents in minutes', 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 28 |