diff --git a/.models b/.models index 7880248a..6f7747fc 100644 --- a/.models +++ b/.models @@ -1,4 +1,6 @@ -gemma:2b -llama3:8b +gemma2:2b +llama3.1:8b +phi3:mini + +#Place any new model here and it will be downloaded during start command -#Place any new model here and it will be downloaded during start command \ No newline at end of file diff --git a/Docs/Images/temp/example.json b/Docs/Images/temp/example.json new file mode 100644 index 00000000..07aed352 --- /dev/null +++ b/Docs/Images/temp/example.json @@ -0,0 +1,59 @@ +{ + "id": "d2f191c7-f08b-4285-b0d6-bb99a045ebde", + "name": "agent_one", + "description": "This is the first RAG agent", + "model": "gemma:2b", + "context": + { + "instruction": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product", + "source": + { + "type": "api", //can be api/sql/text/file as we know it now + "details": + { + "url": "https://api.example.com/@filter@", + "method": "GET", + "query": "", + "payload": "" + //In future we will also have to add authentication details + } + }, + "steps": ["ANSWER"], + //"steps": ["REDIRECT_ac243657-5ab1-4727-b4be-1ea5ae2e76d3", "FETCH_DATA_WITH_FILTER", "ANSWER", "REDIRECT_b29211e9-9ee8-45f4-bdbb-054cb835d0d6"], + "relations": //connections that this agent can make to other agents + [ + { + "id": "ac243657-5ab1-4727-b4be-1ea5ae2e76d3", + "agentPurpose": "category detection", + }, + { + "id": "b29211e9-9ee8-45f4-bdbb-054cb835d0d6", + "agentPurpose": "output prettifier", + } + ] + } +} + +{ + "id": "ac243657-5ab1-4727-b4be-1ea5ae2e76d3", + "name": "agent_two", + "description": "This is the second RAG agent", + "model": "gemma:2b", + "context": + { + "instruction": "Identify correct category for question: Available are PC, LAPTOP, ACCESSORIES - Its important to respond with single word", + "steps": ["ANSWER"], + } +} + +{ + "id": "b29211e9-9ee8-45f4-bdbb-054cb835d0d6", + "name": "agent_three", + "description": "This is the third RAG agent", + "model": "gemma:2b", + "context": + { + "instruction": "Your role is to prettify the response for the user to understand better", + "steps": ["ANSWER"], + } +} \ No newline at end of file diff --git a/Docs/Images/temp/response.json b/Docs/Images/temp/response.json new file mode 100644 index 00000000..3b3be44a --- /dev/null +++ b/Docs/Images/temp/response.json @@ -0,0 +1,13 @@ +{ + "id": "a0353d9d-2dfb-4663-91a4-987a20c9aba1", + "name": "agent_one", + "model": "gemma:2b", + "messages": [ + { + "role": "system", + "content": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product" + } + ], + "type": 2, + "stream": false + } \ No newline at end of file diff --git a/Docs/Images/temp/result.json b/Docs/Images/temp/result.json new file mode 100644 index 00000000..5b74daf7 --- /dev/null +++ b/Docs/Images/temp/result.json @@ -0,0 +1,28 @@ +{ + "Id": "32d8d3f5-4cb5-4f9b-bb51-ab674ae212f5", + "Name": "agent_one", + "Model": "gemma:2b", + "Messages": [ + { + "Role": "system", + "Content": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product" + }, + { + "Role": "user", + "Content": "I am looking for something budget friendly but with options for extensions" + }, + { + "Role": "system", + "Content": "Here is data from internal data source, This is what you should use to answer questions: [{\"id\":1,\"image\":\"pc1.jpg\",\"name\":\"Gaming PC Ultra\",\"brand\":\"PowerTech\",\"processor\":\"Intel Core i9-11900K\",\"type\":\"PC\",\"ram\":\"32GB DDR4\",\"storage\":\"1TB SSD + 2TB HDD\",\"gpu\":\"NVIDIA GeForce RTX 3090\",\"price\":2999.99,\"availability\":\"In Stock\",\"description\":\"The Gaming PC Ultra from PowerTech features an Intel Core i9 processor and NVIDIA RTX 3090, making it perfect for high-end gaming and demanding applications. With 32GB of RAM and ample storage, this PC can handle any task with ease.\"},{\"id\":2,\"image\":\"pc2.png\",\"name\":\"Workstation Pro\",\"brand\":\"TechMaster\",\"processor\":\"AMD Ryzen 9 5950X\",\"type\":\"PC\",\"ram\":\"64GB DDR4\",\"storage\":\"2TB SSD\",\"gpu\":\"NVIDIA Quadro RTX 5000\",\"price\":3499.99,\"availability\":\"In Stock\",\"description\":\"TechMaster's Workstation Pro is designed for professionals who require top-tier performance. With an AMD Ryzen 9 CPU, 64GB RAM, and NVIDIA Quadro RTX GPU, this workstation excels in 3D rendering, video editing, and other intensive tasks.\"},{\"id\":3,\"image\":\"pc3.jpg\",\"name\":\"Budget Gamer\",\"brand\":\"EconoPC\",\"processor\":\"Intel Core i5-10400F\",\"type\":\"PC\",\"ram\":\"16GB DDR4\",\"storage\":\"512GB SSD\",\"gpu\":\"NVIDIA GeForce GTX 1660 Super\",\"price\":799.99,\"availability\":\"Out of Stock\",\"description\":\"The Budget Gamer by EconoPC provides excellent value for entry-level gaming. Featuring an Intel Core i5 processor and NVIDIA GTX 1660 Super GPU, this PC delivers smooth performance for popular games at an affordable price.\"},{\"id\":4,\"image\":\"pc4.jpg\",\"name\":\"All-Purpose PC\",\"brand\":\"ValueComp\",\"processor\":\"AMD Ryzen 5 3600\",\"type\":\"PC\",\"ram\":\"16GB DDR4\",\"storage\":\"1TB HDD\",\"gpu\":\"AMD Radeon RX 5700\",\"price\":999.99,\"availability\":\"In Stock\",\"description\":\"ValueComp's All-Purpose PC is a versatile system suitable for gaming, work, and everyday use. It features an AMD Ryzen 5 processor, 16GB RAM, and a Radeon RX 5700 GPU, ensuring balanced performance for a variety of tasks.\"},{\"id\":5,\"image\":\"pc5.jpg\",\"name\":\"Compact Office PC\",\"brand\":\"OfficeMate\",\"processor\":\"Intel Core i3-10100\",\"type\":\"PC\",\"ram\":\"8GB DDR4\",\"storage\":\"256GB SSD\",\"gpu\":\"Integrated Graphics\",\"price\":499.99,\"availability\":\"In Stock\",\"description\":\"The Compact Office PC by OfficeMate is ideal for small spaces and routine office tasks. Equipped with an Intel Core i3 processor and 8GB of RAM, it offers reliable performance for word processing, spreadsheets, and internet browsing.\"},{\"id\":6,\"image\":\"pc6.jpg\",\"name\":\"Performance Beast\",\"brand\":\"ExtremeTech\",\"processor\":\"Intel Core i7-12700K\",\"type\":\"PC\",\"ram\":\"32GB DDR5\",\"storage\":\"2TB SSD\",\"gpu\":\"NVIDIA GeForce RTX 3080\",\"price\":2599.99,\"availability\":\"In Stock\",\"description\":\"ExtremeTech's Performance Beast is designed for the most demanding users. With an Intel Core i7 processor, 32GB DDR5 RAM, and NVIDIA RTX 3080 GPU, this PC is ideal for gaming, 3D rendering, and other high-performance applications.\"},{\"id\":7,\"image\":\"laptop1.jpg\",\"name\":\"UltraBook Pro\",\"brand\":\"PowerTech\",\"processor\":\"Intel Core i7-1165G7\",\"type\":\"Laptop\",\"ram\":\"16GB LPDDR4x\",\"storage\":\"512GB SSD\",\"gpu\":\"Intel Iris Xe Graphics\",\"price\":1299.99,\"availability\":\"In Stock\",\"description\":\"The UltraBook Pro by PowerTech offers a sleek design with powerful performance, featuring an Intel Core i7 processor and Intel Iris Xe Graphics. With 16GB of RAM and a 512GB SSD, this laptop is perfect for professionals on the go.\"},{\"id\":8,\"image\":\"laptop2.jpg\",\"name\":\"Gaming Laptop X\",\"brand\":\"TechMaster\",\"processor\":\"AMD Ryzen 7 5800H\",\"type\":\"Laptop\",\"ram\":\"32GB DDR4\",\"storage\":\"1TB SSD\",\"gpu\":\"NVIDIA GeForce RTX 3070\",\"price\":1999.99,\"availability\":\"In Stock\",\"description\":\"TechMaster's Gaming Laptop X is designed for gamers who need high performance on the go. Featuring an AMD Ryzen 7 CPU and NVIDIA RTX 3070 GPU, this laptop delivers smooth gaming experiences and fast load times.\"},{\"id\":9,\"image\":\"laptop3.jpg\",\"name\":\"Budget Laptop\",\"brand\":\"EconoPC\",\"processor\":\"Intel Core i3-1115G4\",\"type\":\"Laptop\",\"ram\":\"8GB DDR4\",\"storage\":\"256GB SSD\",\"gpu\":\"Integrated Graphics\",\"price\":499.99,\"availability\":\"Out of Stock\",\"description\":\"The Budget Laptop by EconoPC offers essential features at an affordable price. With an Intel Core i3 processor and 8GB of RAM, it's suitable for students and casual users who need reliable performance for everyday tasks.\"},{\"id\":10,\"image\":\"laptop4.jpg\",\"name\":\"All-Purpose Laptop\",\"brand\":\"ValueComp\",\"processor\":\"AMD Ryzen 5 4500U\",\"type\":\"Laptop\",\"ram\":\"16GB DDR4\",\"storage\":\"512GB SSD\",\"gpu\":\"AMD Radeon Graphics\",\"price\":899.99,\"availability\":\"In Stock\",\"description\":\"ValueComp's All-Purpose Laptop is versatile and powerful, equipped with an AMD Ryzen 5 processor and Radeon Graphics. It's ideal for a variety of tasks, including work, entertainment, and light gaming.\"},{\"id\":11,\"image\":\"laptop5.jpg\",\"name\":\"Compact Office Laptop\",\"brand\":\"OfficeMate\",\"processor\":\"Intel Core i5-1135G7\",\"type\":\"Laptop\",\"ram\":\"8GB LPDDR4x\",\"storage\":\"512GB SSD\",\"gpu\":\"Intel Iris Xe Graphics\",\"price\":799.99,\"availability\":\"In Stock\",\"description\":\"The Compact Office Laptop by OfficeMate is perfect for professionals who need a portable and efficient device. With an Intel Core i5 processor and 8GB of RAM, it offers solid performance for office applications and multitasking.\"},{\"id\":12,\"image\":\"laptop6.jpg\",\"name\":\"Creative Laptop\",\"brand\":\"CreativeTech\",\"processor\":\"Apple M1\",\"type\":\"Laptop\",\"ram\":\"16GB Unified Memory\",\"storage\":\"1TB SSD\",\"gpu\":\"Apple M1 GPU\",\"price\":1499.99,\"availability\":\"In Stock\",\"description\":\"CreativeTech's Creative Laptop is perfect for designers and content creators. Featuring the Apple M1 chip, 16GB of unified memory, and a 1TB SSD, this laptop offers exceptional performance and efficiency for creative tasks.\"}]" + }, + { + "Role": "assistant", + "Content": "## Budget Gamer: Your Gateway to High-Performance Gaming on a Shoestring\n\nThe **Budget Gamer** from EconoPC is the PC that proves you can have the ultimate gaming experience without breaking the bank. It's packed with powerful features and sleek design, making it the perfect companion for both casual gamers and hardcore enthusiasts.\n\n**Here's what the Budget Gamer offers:**\n\n* **Unmatched value for its price:** This PC delivers high performance and exceptional features without the hefty price tag.\n* **Next-gen specs:** It boasts a brand-new Ryzen 7 5800U processor, 16GB of fast DDR5 memory, and a NVIDIA GeForce RTX 3060 graphics card – enough to run most popular games at high settings and even some demanding titles in 1080p.\n* **Ultra-quiet operation:** Enjoy a peaceful gaming environment with the built-in fans that ensure minimal noise.\n* **Sleek and stylish design:** The Budget Gamer boasts an elegant chassis and a unique mesh front panel, giving it a modern touch.\n* **Warranty and support:** EconoPC offers a comprehensive warranty and dedicated support resources, ensuring you have everything you need for complete peace of mind.\n\n**The Budget Gamer is perfect for:**\n\n* Gamers on a tight budget who still want to enjoy high-quality gaming.\n* First-time PC builders who want a powerful system without breaking the bank.\n* Gamers who demand high performance and a quiet and comfortable gaming environment.\n\n**Don't miss out on the ultimate gaming experience – get your Budget Gamer today!**" + } + ], + "Type": 2, + "Stream": false, + "Properties": { + "FETCH_DATA*": "" + } + } \ No newline at end of file diff --git a/Docs/Images/temp/test.json b/Docs/Images/temp/test.json new file mode 100644 index 00000000..69acafbc --- /dev/null +++ b/Docs/Images/temp/test.json @@ -0,0 +1,40 @@ +[ + { + "id": "d2f191c7-f08b-4285-b0d6-bb99a045ebde", + "name": "agent_one", + "description": "This is the first RAG agent", + "model": "llama3.1:8b", + "context": + { + "instruction": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product", + "source": + { + "type": 1, + "details": + { + "url": "http://localhost:5098/items", + "method": "Get", + "query": "", + "payload": "" + } + }, + "steps": ["FETCH_DATA*","ANSWER", "REDIRECT+b29211e9-9ee8-45f4-bdbb-054cb835d0d6+AS_Output+REPLACE"], + "relations": + [ + "ac243657-5ab1-4727-b4be-1ea5ae2e76d3", + "b29211e9-9ee8-45f4-bdbb-054cb835d0d6" + ] + } +}, +{ + "id": "b29211e9-9ee8-45f4-bdbb-054cb835d0d6", + "name": "agent_three", + "description": "This is the third RAG agent", + "model": "llama3.1:8b", + "context": + { + "instruction": "Adjust previous response to be better for marketting purposes. Dont include any introduction, just pure content", + "steps": ["ANSWER"] + } +} +] \ No newline at end of file diff --git a/Frontend/MainFE/.idea/.idea.MainFE/.idea/inspectionProfiles/Project_Default.xml b/Frontend/MainFE/.idea/.idea.MainFE/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..39b37c89 --- /dev/null +++ b/Frontend/MainFE/.idea/.idea.MainFE/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/Frontend/MainFE/Components/Elements/ChatComponent.razor b/Frontend/MainFE/Components/Elements/ChatComponent.razor new file mode 100644 index 00000000..2a495ba0 --- /dev/null +++ b/Frontend/MainFE/Components/Elements/ChatComponent.razor @@ -0,0 +1,114 @@ +@using MainFE.Components.Models +@using Markdig +@using Microsoft.FluentUI.AspNetCore.Components +@using Message = MainFE.Components.Models.Message +@inject HttpClient Http + + +
+ @foreach (var message in Chat?.Messages ?? []) + { + @if (message.Role != Role.System.ToString().ToLowerInvariant()) + { + @GetRoleLabel(message.Role) + + @((MarkupString)((message.Role == Role.User.ToString() + ? message.Content + : Markdown.ToHtml((string)message.Content!, + new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build())) ?? string.Empty)) + + } + } + @if (IsLoading) + { + @_displayName + + } +
+
+ + + +
+ + +@code +{ + [Parameter] public ChatDto? Chat { get; set; } + [Parameter] public string SelectedModel { get; set; } + [Parameter] public bool IsLoading { get; set; } + [Parameter] public string? CustomName { get; set; } + [Parameter] public bool IsRagChat { get; set; } + [Parameter] public string? AgentId { get; set; } + [Parameter] public bool Translate { get; set; } + + private string _ask = string.Empty; + private string _displayName => CustomName ?? SelectedModel; + + private async Task SendAsync(string message) + { + if (IsRagChat) + { + await RaGSendAsync(message); + return; + } + + IsLoading = true; + if (!string.IsNullOrWhiteSpace(message)) + { + Chat.Messages.Add(new Message { Role = Role.User.ToString(), Content = message }); + Chat.Model = SelectedModel; + + var apiResponse = await Http.PostAsJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/chats/complete?translate={Translate}", Chat); + + if (apiResponse.IsSuccessStatusCode) + { + var response = await apiResponse.Content.ReadFromJsonAsync(); + if (response != null) + { + Chat.Messages.Add(response.Message); + } + } + IsLoading = false; + _ask = string.Empty; + } + } + + private async Task RaGSendAsync(string message) + { + IsLoading = true; + if (!string.IsNullOrWhiteSpace(message)) + { + Chat.Messages.Add(new Message { Role = Role.User.ToString(), Content = message }); + Chat.Model = SelectedModel; + + var apiResponse = await Http.PostAsJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{AgentId}/process", Chat); + + if (apiResponse.IsSuccessStatusCode) + { + var response = await apiResponse.Content.ReadFromJsonAsync(); + if (response != null) + { + Chat = response; + } + } + IsLoading = false; + _ask = string.Empty; + } + } + + private async Task CheckEnterKey(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await SendAsync(_ask); + } + } + + private string GetRoleLabel(string role) => role == Role.User.ToString() ? "( ͡° ͜ʖ ͡°) User" : $"{_displayName}"; +} diff --git a/Frontend/MainFE/Components/Elements/TileSelect.razor b/Frontend/MainFE/Components/Elements/TileSelect.razor new file mode 100644 index 00000000..f017227e --- /dev/null +++ b/Frontend/MainFE/Components/Elements/TileSelect.razor @@ -0,0 +1,51 @@ +@typeparam TItem + +
+ @foreach (var item in Items) + { +
+ @item +
+ } +
+ +@code { + [Parameter] public List Items { get; set; } + [Parameter] public TItem SelectedItem { get; set; } + [Parameter] public EventCallback SelectedItemChanged { get; set; } + + private void SelectItem(TItem item) + { + SelectedItem = item; + SelectedItemChanged.InvokeAsync(item); + } +} + + \ No newline at end of file diff --git a/Frontend/MainFE/Components/Layout/NavMenu.razor b/Frontend/MainFE/Components/Layout/NavMenu.razor index c4941d10..50a4286d 100644 --- a/Frontend/MainFE/Components/Layout/NavMenu.razor +++ b/Frontend/MainFE/Components/Layout/NavMenu.razor @@ -4,31 +4,17 @@ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); background-color: #1e1e1e !important; } - - .select-label { - color: #fff; - margin-right: 10px; - } - - .select-element { - background-color: #2e2e2e; - color: #fff; - border: 1px solid #333; - border-radius: 4px; - padding: 6px 12px; - font-size: 16px; - appearance: none; - } - - .select-element:hover { - background-color: #4a4a4a; + + .disabledNav{ + cursor: not-allowed !important; + pointer-events: none; + color: #6c757d; } - - .select-element:focus { - outline: none; - border-color: #0078D4; - box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.3); + + .enabled{ + color: #ffffff !important; } + @code { diff --git a/Frontend/MainFE/Components/Models/AgentContextDto.cs b/Frontend/MainFE/Components/Models/AgentContextDto.cs new file mode 100644 index 00000000..d056fded --- /dev/null +++ b/Frontend/MainFE/Components/Models/AgentContextDto.cs @@ -0,0 +1,10 @@ +namespace MaIN.Models.Rag; + +public class AgentContextDto +{ + public string Instruction { get; set; } + public object? Source { get; set; } + public List Steps { get; set; } + public List? Relations { get; set; } + +} \ No newline at end of file diff --git a/Frontend/MainFE/Components/Models/AgentDto.cs b/Frontend/MainFE/Components/Models/AgentDto.cs new file mode 100644 index 00000000..7f255fb4 --- /dev/null +++ b/Frontend/MainFE/Components/Models/AgentDto.cs @@ -0,0 +1,11 @@ +namespace MaIN.Models.Rag; + +public class AgentDto +{ + public string Id { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public string? Description { get; set; } + public bool Started { get; set; } + public AgentContextDto Context { get; set; } +} \ No newline at end of file diff --git a/Frontend/MainFE/Components/Models/AgentsPageMode.cs b/Frontend/MainFE/Components/Models/AgentsPageMode.cs new file mode 100644 index 00000000..c11ff2b1 --- /dev/null +++ b/Frontend/MainFE/Components/Models/AgentsPageMode.cs @@ -0,0 +1,9 @@ +namespace MainFE.Components.Models; + +public enum AgentsPageMode +{ + List, + Details, + Create, + Demo, +} \ No newline at end of file diff --git a/Frontend/MainFE/Components/Models/ChatDto.cs b/Frontend/MainFE/Components/Models/ChatDto.cs index d24ac62a..d0841fb2 100644 --- a/Frontend/MainFE/Components/Models/ChatDto.cs +++ b/Frontend/MainFE/Components/Models/ChatDto.cs @@ -10,5 +10,7 @@ public class ChatDto [JsonPropertyName("model")] public string Model { get; set; } [JsonPropertyName("messages")] public List Messages { get; set; } [JsonPropertyName("stream")] public bool Stream { get; set; } = false; + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } [JsonIgnore] public bool IsSelected { get; set; } } \ No newline at end of file diff --git a/Frontend/MainFE/Components/Models/HardwareItemDto.cs b/Frontend/MainFE/Components/Models/HardwareItemDto.cs new file mode 100644 index 00000000..f825ec6f --- /dev/null +++ b/Frontend/MainFE/Components/Models/HardwareItemDto.cs @@ -0,0 +1,18 @@ +namespace MainFE.Components.Models; + +public class HardwareItemDto +{ + public int Id { get; set; } + public string Type { get; set; } + public string Name { get; set; } + public string Brand { get; set; } + public string Processor { get; set; } + public string Ram { get; set; } + public string Storage { get; set; } + public string Gpu { get; set; } + public double Price { get; set; } + public string Availability { get; set; } + public string Description { get; set; } + public string Image { get; set; } + public bool IsDetailsVisible { get; set; } +} \ No newline at end of file diff --git a/Frontend/MainFE/Components/Pages/Agents.razor b/Frontend/MainFE/Components/Pages/Agents.razor new file mode 100644 index 00000000..737f3be3 --- /dev/null +++ b/Frontend/MainFE/Components/Pages/Agents.razor @@ -0,0 +1,272 @@ +@page "/agents" +@using MaIN.Models.Rag +@using MainFE.Components.Models +@using Microsoft.FluentUI.AspNetCore.Components +@using MainFE.Components.Elements +@inject HttpClient Http +@inject NavigationManager Nav +@rendermode InteractiveServer + +👥 Agents + + + + +@switch (_mode) +{ + case AgentsPageMode.List: + + + +

Predefined Agents

+
+ + + + @foreach (var agent in agents.Where(x => _predefinedAgents.Contains(x.Id))) + { + @if (agent.Id == "b29211e9-9ee8-45f4-bdbb-054cb835d0d6") + { + +
+
@agent.Model
+ 👑 +

@agent.Description

+
+
+ } + @if (agent.Id == "c39211w9-9ee8-4xf4-edbb-b54cb835d2d6") + { + +
+
@agent.Model
+ 🎮 +

@agent.Description

+
+
+ } + @if (agent.Id == "vd9d11w9-9ee8-4xf4-edbb-b54cb335d25b") + { + +
+
@agent.Model
+ 👨‍⚕️ +

@agent.Description

+
+
+ } + } + @if (agents.Any(x => !_predefinedAgents.Contains(x.Id))) + { + + +

Custom Agents

+
+ + } + @foreach (var agent in agents.Where(x => !_predefinedAgents.Contains(x.Id))) + { + +
+ +
@agent.Model
+ +

@agent.Description

+
+
+ } + + + + + + +
+ break; + case AgentsPageMode.Create: + + + + + +

Create new agent ✨

+
+ +
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ @if(!_loading) + { + + } + else + { + + } +
+
+
+
+ break; + case AgentsPageMode.Details: + + + + + +

@selectedAgent.Name

+
+ + + + +

@selectedAgent.Description

+
+ + @if (selectedAgent.Id == "b29211e9-9ee8-45f4-bdbb-054cb835d0d6") + { + + } + else if (selectedAgent.Id == "c39211w9-9ee8-4xf4-edbb-b54cb835d2d6") + { + + } + else if (selectedAgent.Id == "vd9d11w9-9ee8-4xf4-edbb-b54cb335d25b") + { + + } + else + { + + } + +
+ break; + case AgentsPageMode.Demo: + break; +} + +@code { + + private List _predefinedAgents = ["d2f191c7-f08b-4285-b0d6-bb99a045ebde","f29211e9-9xe8-45f4-bdbb-054cb835d0d6","b29211e9-9ee8-45f4-bdbb-054cb835d0d6", "c39211w9-9ee8-4xf4-edbb-b54cb835d2d6", "vd9d11w9-9ee8-4xf4-edbb-b54cb335d25b"]; + + // Models + AgentDto newAgent; + List models = new(); + List agents = new(); + string _selectedModel = string.Empty; + ChatDto _selectedChat = new() { Messages = new() }; + AgentDto selectedAgent = new(); + AgentsPageMode _mode = AgentsPageMode.List; + bool _loading = false; + + protected override async Task OnInitializedAsync() + { + newAgent = new() + { + Context = new() + }; + + Http.Timeout = TimeSpan.FromMinutes(10); + await LoadAgentsAsync(); + } + + private async Task LoadChatAsync(string agentId) + { + var response = await Http.GetFromJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{agentId}/chat"); + if (response != null) + { + _selectedChat = response; + _selectedModel = response.Model; + } + } + + private async Task RemoveAgent(string agentId) + { + await Http.DeleteAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{agentId}"); + await LoadAgentsAsync(); + } + + private async Task Cleanup(string selectedAgentId) + { + await Http.PutAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{selectedAgentId}/chat/reset", default); + await LoadChatAsync(selectedAgentId); + } + + private async Task LoadAgentsAsync() + { + var response = await Http.GetFromJsonAsync>($"{ExtensionMethods.GetApiUrl()}/api/agents"); + if (response != null) + { + agents = response; + } + } + + private async Task ViewAgentDetails(string agentId) + { + selectedAgent = agents.FirstOrDefault(a => a.Id == agentId)!; + _mode = AgentsPageMode.Details; + await LoadChatAsync(selectedAgent.Id); // Assuming each agent has a ChatId property + } + + private async Task CreateAgent() + { + _loading = true; + newAgent.Context.Steps = ["START"]; + newAgent.Id = Guid.NewGuid().ToString(); + var response = await Http.PostAsJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/agents", newAgent); + if (response.IsSuccessStatusCode) + { + newAgent = new(); // Reset the form + await LoadAgentsAsync(); // Refresh the agents list + _loading = false; + _mode = AgentsPageMode.List; // Switch back to list mode + } + } + + private void GoBack() + { + _mode = AgentsPageMode.List; + } + + private void SwitchToCreate() + { + _mode = AgentsPageMode.Create; + } + + private void NavigateToDemo() + { + Nav.NavigateTo("/rag"); + } + +} + diff --git a/Frontend/MainFE/Components/Pages/Chat.razor b/Frontend/MainFE/Components/Pages/Chat.razor index eb0aaf93..d11fd3ba 100644 --- a/Frontend/MainFE/Components/Pages/Chat.razor +++ b/Frontend/MainFE/Components/Pages/Chat.razor @@ -4,12 +4,12 @@ @using Microsoft.FluentUI.AspNetCore.Components @using NAssetNameGenerator @using Message = MainFE.Components.Models.Message +@using MainFE.Components.Elements @inject HttpClient Http -@rendermode InteractiveServer +@rendermode @(new InteractiveServerRenderMode(prerender: false)) 💬 Chat - New Chat - @foreach (var chat in chats) + @foreach (var chat in _chats) {
} - -
- @foreach (var message in messages) - { - @GetRoleLabel(message.Role) - - @((MarkupString)((message.Role == Role.User.ToString() ? message.Content : - Markdown.ToHtml((string)message.Content!, - new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build())) ?? string.Empty)) - - } - @if (loading) - { - @selectedModel - - } -
-
- - - -
- + + - - Model: - + +

Select a Model

+ + +
+

Multilingual

+
+ Improve translations + + +
+
@code { + // Models - List models = []; - string selectedModel = string.Empty; + bool _translate = false; + List _models = []; + string _selectedModel = string.Empty; + bool _loading = false; // Data - List messages = new(); - List chats = new(); - ChatDto selectedChat = new(); - string ask = string.Empty; - bool loading = false; + List _chats = new(); - private async Task CheckEnterKey(KeyboardEventArgs e) + ChatDto _selectedChat = new() { - if (e.Key == "Enter") - { - await SendAsync(ask); - } - } + Messages = [] + }; + private void OnModelSelected(string model) + { + _selectedModel = model; + } + protected override async Task OnInitializedAsync() { - Http.Timeout = TimeSpan.FromMinutes(10); await LoadChatsAsync(); await LoadModelsAsync(); } private async Task LoadChatsAsync() { - var response = await Http.GetAsync($"{ExtensionMethods.GetApiUrl()}/api/chats"); if (response.IsSuccessStatusCode) { - chats = await response.Content.ReadFromJsonAsync>() ?? []; + _chats = await response.Content.ReadFromJsonAsync>() ?? []; } } @@ -113,8 +90,8 @@ var response = await Http.GetAsync($"{ExtensionMethods.GetApiUrl()}/api/chats/models"); if (response.IsSuccessStatusCode) { - models = await response.Content.ReadFromJsonAsync>() ?? []; - selectedModel = models!.First(); + _models = await response.Content.ReadFromJsonAsync>() ?? []; + _selectedModel = _models!.First(); } } @@ -123,9 +100,8 @@ var response = await Http.GetFromJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/chats/{chatId}"); if (response != null) { - messages = response.Messages; - selectedChat = response; - chats.ForEach(c => c.IsSelected = c.Id == chatId); + _selectedChat = response; + _chats.ForEach(c => c.IsSelected = c.Id == chatId); } } @@ -134,21 +110,19 @@ var response = await Http.DeleteAsync($"{ExtensionMethods.GetApiUrl()}/api/chats/{id}"); if (response.IsSuccessStatusCode) { - chats.RemoveAll(c => c.Id == id); - if (selectedChat.Id == id) + _chats.RemoveAll(c => c.Id == id); + if (_selectedChat.Id == id) { - selectedChat = new(); - messages.Clear(); + _selectedChat = new(); } } } private async Task NewChatAsync() { - messages.Clear(); var newChatRequest = new ChatRequest { - Model = selectedModel, + Model = _selectedModel, Messages = new List(), Name = $"{AssetName.NewName()} | {DateTime.Now.ToShortDateString()}", Stream = false, @@ -162,38 +136,13 @@ if (response != null) { response.IsSelected = true; - selectedChat = response; - chats.Add(response); + _selectedChat = response; + _chats.Add(response); await LoadChatAsync(response.Id); } } } - private async Task SendAsync(string message) - { - loading = true; - if (!string.IsNullOrWhiteSpace(message)) - { - messages.Add(new Message { Role = Role.User.ToString(), Content = message }); - selectedChat.Messages = messages; - selectedChat.Model = selectedModel; - - var replyServerOllama = await Http.PostAsJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/chats/complete", selectedChat); - - if (replyServerOllama.IsSuccessStatusCode) - { - var response = await replyServerOllama.Content.ReadFromJsonAsync(); - if (response != null) - { - messages.Add(response.Message); - } - } - loading = false; - ask = string.Empty; - } - } - // Helper methods private string GetChatCardClass(ChatDto chat) => chat.IsSelected ? "selected-chat-card" : "chat-card"; - private string GetRoleLabel(string role) => role == Role.User.ToString() ? "( ͡° ͜ʖ ͡°) User" : $"🤖 {selectedModel}"; } \ No newline at end of file diff --git a/Frontend/MainFE/Components/Pages/Home.razor b/Frontend/MainFE/Components/Pages/Home.razor index 9dbc6ebc..60549d27 100644 --- a/Frontend/MainFE/Components/Pages/Home.razor +++ b/Frontend/MainFE/Components/Pages/Home.razor @@ -1,28 +1,25 @@ @page "/" + Home
-
- Robot +
+ Robot +
+

Welcome to M.A.I.N

-

Welcome to M.A.I.N

-
-

+

M.A.I.N (Modular Artificial Intelligence Network) is an advanced, modular AI platform designed to integrate various AI capabilities seamlessly. Initially featuring chat-based solutions powered by open-source large language models (LLMs), M.A.I.N is set to evolve with additional functionalities, providing a comprehensive AI toolkit for diverse applications. The ultimate goal is to build an AI that is aware of what is happening within the system and can react to those changes. This interconnectedness and contextual awareness is what makes it a "Network."

-
-
    -
  • Chat Solutions: Leveraging open-source LLMs to deliver sophisticated conversational AI experiences.
  • -
  • Vision Capabilities:Upcoming feature enabling the system to process and interpret visual data.
  • -
  • Image Processing: Advanced image handling and analysis tools.
  • -
  • Retrieval-Augmented Generation (RAG): Integrating retrieval mechanisms to enhance response generation with up-to-date information.
  • -
  • Modular Architecture: Flexible and scalable design allowing easy integration of new AI modules.
  • -
  • Open-Source Foundation: Built on robust, community-driven AI technologies.
  • -
-
+
+
diff --git a/Frontend/MainFE/Components/Pages/Rag/RAG.razor b/Frontend/MainFE/Components/Pages/Rag/RAG.razor new file mode 100644 index 00000000..4bf0ef7a --- /dev/null +++ b/Frontend/MainFE/Components/Pages/Rag/RAG.razor @@ -0,0 +1,201 @@ +@page "/RAG" +@using System.Text.Json +@using MaIN.Models.Rag +@using MainFE.Components.Models +@using Markdig +@using Microsoft.FluentUI.AspNetCore.Components +@using Message = MainFE.Components.Models.Message +@using MainFE.Components.Elements +@rendermode @(new InteractiveServerRenderMode(prerender: false)) +@inject HttpClient Http + + + + +@if (_globalLoading) +{ + +} +else +{ +
+
+ +
+ + +
+
+ @foreach (var item in _items) + { + +
+ @item.Name +

@item.Name

+

+ Brand: @item.Brand +

+

+ Type: @item.Type +

+ +
+
+ } +
+ + + + + + +} + +@code { + ChatDto _selectedChat = new(); + string _ask = string.Empty; + bool _loading = false; + bool _globalLoading = true; + AgentDto _selectedAgent; + string _designatedAgent = "agent_one"; + + HardwareItemDto selectedItem = new(); + private List _items = new(); + + protected override async Task OnInitializedAsync() + { + try + { + Http.Timeout = TimeSpan.FromMinutes(10); + await InitRagAsync(); + await LoadProductsAsync(); + } + finally + { + _globalLoading = false; + } + } + + private async Task LoadProductsAsync() + { + var response = await Http.GetAsync($"{ExtensionMethods.GetDemoApiUrl()}/items/"); + if (response.IsSuccessStatusCode) + { + _items = await response.Content.ReadFromJsonAsync>() ?? []; + } + } + + private void ShowDetailsModal(HardwareItemDto item) + { + selectedItem = item; + } + + + private async Task InitRagAsync() + { + var filePath = ExtensionMethods.GetWorkingEnvironment() == "Docker" ? "wwwroot/initial_rag_demo_docker.json" : "wwwroot/initial_rag_demo.json"; + var agents = JsonSerializer.Deserialize>( + await File.ReadAllTextAsync(filePath), new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + _selectedAgent = agents?.FirstOrDefault(x => x.Name == _designatedAgent)!; + foreach (var agent in agents!) + { + await Http.PostAsJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/agents", agent); + } + + _selectedChat = (await Http.GetFromJsonAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{_selectedAgent.Id}/chat"))!; + } + + private async Task Cleanup(string selectedAgentId) + { + await Http.PutAsync($"{ExtensionMethods.GetApiUrl()}/api/agents/{selectedAgentId}/chat/reset", default); + _selectedChat.Properties.Clear(); + _selectedChat.Messages = _selectedChat.Messages.Take(1).ToList(); + } +} diff --git a/Frontend/MainFE/ExtensionMethods.cs b/Frontend/MainFE/ExtensionMethods.cs index f50e5d4b..88b0c755 100644 --- a/Frontend/MainFE/ExtensionMethods.cs +++ b/Frontend/MainFE/ExtensionMethods.cs @@ -45,5 +45,10 @@ public static string GetApiUrl() { return Environment.GetEnvironmentVariable("API_URL") ?? throw new InvalidOperationException("API_URL environment variable is not set"); } + + public static string GetDemoApiUrl() + { + return Environment.GetEnvironmentVariable("DEMO_API_URL") ?? throw new InvalidOperationException("DEMO_API_URL environment variable is not set"); + } } \ No newline at end of file diff --git a/Frontend/MainFE/Properties/launchSettings.json b/Frontend/MainFE/Properties/launchSettings.json index 0715fa45..19120832 100644 --- a/Frontend/MainFE/Properties/launchSettings.json +++ b/Frontend/MainFE/Properties/launchSettings.json @@ -16,6 +16,7 @@ "applicationUrl": "http://localhost:5028", "environmentVariables": { "API_URL": "http://localhost:5243", + "DEMO_API_URL": "http://localhost:5098", "ASPNETCORE_ENVIRONMENT": "Development" } }, diff --git a/Frontend/MainFE/appsettings.Docker.json b/Frontend/MainFE/appsettings.Docker.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Frontend/MainFE/appsettings.Docker.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Frontend/MainFE/wwwroot/app.css b/Frontend/MainFE/wwwroot/app.css index 6c000a6a..816370a2 100644 --- a/Frontend/MainFE/wwwroot/app.css +++ b/Frontend/MainFE/wwwroot/app.css @@ -35,8 +35,32 @@ code { color: #a9beff !important; } +.navl { + display: inline; + padding: .5rem 1rem; + color: #0d6efd; + text-decoration: none; + transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out +} + +@media (prefers-reduced-motion: reduce) { + .navl { + transition: none + } +} + +.navl:focus, .nav-link:hover { + color: #0a58ca +} + +.navl.disabled { + color: #6c757d; + pointer-events: none; + cursor: default +} + .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { @@ -65,9 +89,9 @@ h1:focus { color: white; } - .blazor-error-boundary::after { - content: "An error has occurred." - } +.blazor-error-boundary::after { + content: "An error has occurred." +} .darker-border-checkbox.form-check-input { border-color: #929292; diff --git a/Frontend/MainFE/wwwroot/css/agents.css b/Frontend/MainFE/wwwroot/css/agents.css new file mode 100644 index 00000000..dcdbebb2 --- /dev/null +++ b/Frontend/MainFE/wwwroot/css/agents.css @@ -0,0 +1,171 @@ +body { + background-color: #121212; + color: #ffffff; + font-family: Arial, sans-serif; +} + +.remove-btn { + position: absolute; + top: 1px; + right: 1px; + background: transparent; + border: none; + color: #fff6a3; + cursor: pointer; +} + +.remove-btn:hover { + color: #ea7373; +} + +.main-container { + padding: 0 20px; +} + +.agents-grid { + padding: 20px; + justify-content: center; + height: 45rem; +} + +.agent-tile { + background-color: #2b2b2b; + border: 1px solid #3c3c3c; + border-radius: 8px; + padding: 20px !important; + margin: 10px !important; + text-align: center; + cursor: pointer; + transition: transform 0.2s, background-color 0.2s; +} + +.agent-tile:hover { + transform: scale(1.05); + background-color: #383838; +} + +.agent-content { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.agent-emoji { + font-size: 2em; + margin-bottom: 10px; +} + +.back-button { + background-color: transparent; + border: none; + color: #00dcaa; /* Primary blue color */ + font-size: 3rem; + cursor: pointer; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.3s ease; +} + +.back-button:hover { + color: #5ff158; /* Darker blue color for hover effect */ +} + +.back-button i { + margin-right: 8px; /* Space between the icon and text (if any) */ +} + +.cleanup-button { + background-color: #4CAF50; /* Green background */ + border: none; /* Remove borders */ + color: white; /* White text */ + padding: 10px 10px; /* Some padding */ + text-align: center; /* Centered text */ + text-decoration: none; /* Remove underline */ + display: inline-block; /* Get the element to behave like an inline-block */ + font-size: 13px; /* Increase font size */ + cursor: pointer; /* Pointer/hand icon */ + border-radius: 8px; /* Rounded corners */ + transition-duration: 0.4s; /* Smooth transition */ +} + +.cleanup-button:hover { + background-color: white; /* White background on hover */ + color: black; /* Black text on hover */ + border: 2px solid #4CAF50; /* Green border on hover */ +} + +.cleanup-button i { + margin-right: 8px; /* Space between icon and text */ +} + +.badge-agent-model { + top: 10px; + right: 10px; + background-color: #8df18d; /* Primary blue color */ + color: #4c4c4c; + padding: 5px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: bold; +} + +.create-form-container { + max-width: 600px; + margin: auto; + padding: 20px; + background-color: #2b2b2b; + border: 1px solid #3c3c3c; + border-radius: 8px; +} + +.create-form-container label { + color: #cccccc; + margin-bottom: -10px; +} + +.create-form-container .form-group { + margin-bottom: 15px; +} + +.create-form-container input, +.create-form-container textarea { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #333; + border-radius: 4px; + background-color: #121212; + color: #ffffff; +} + +.create-form-container input:focus, +.create-form-container textarea:focus { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #af6bff; + border-radius: 4px; + background-color: #121212; + color: #ffffff; +} + + +.create-button { + background-color: #7aeda3; + color: #3a3232; + border: none; + cursor: pointer; + padding: 10px 20px; + border-radius: 4px; + width: 15rem; + transition: background-color 0.2s; + display: block; + margin: auto; +} + +.create-button:hover { + background-color: #005a9e; +} diff --git a/Frontend/MainFE/wwwroot/css/chat.css b/Frontend/MainFE/wwwroot/css/chat.css index 52b2d4f7..e0f52f77 100644 --- a/Frontend/MainFE/wwwroot/css/chat.css +++ b/Frontend/MainFE/wwwroot/css/chat.css @@ -12,7 +12,6 @@ .containerbox { display: flex; height: 90vh; - width: 90vw !important; flex-direction: column; } @@ -88,7 +87,7 @@ } .input-container { - max-height: 7%; + height: 3rem; display: flex; padding: 5px; position: relative; @@ -106,14 +105,70 @@ margin: 0; } + h1 { + color: green; + } + +/* To hide the checkbox */ +#checkboxInput { + display: none; +} + +.containerToggle { + display: flex; + align-items: center; +} + +.text { + color: white; /* Make the text white */ + margin-right: 10px; /* Space between text and label */ +} + +.toggleSwitch { + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 50px; + height: 30px; + background-color: rgb(82, 82, 82); + border-radius: 20px; + cursor: pointer; + transition-duration: .2s; +} + +.toggleSwitch::after { + content: ""; + position: absolute; + height: 10px; + width: 10px; + left: 5px; + background-color: transparent; + border-radius: 50%; + transition-duration: .2s; + box-shadow: 5px 2px 7px rgba(8, 8, 8, 0.26); + border: 5px solid white; +} + +#checkboxInput:checked+.toggleSwitch::after { + transform: translateX(200%); + transition-duration: .2s; + background-color: white; +} +/* Switch background change */ +#checkboxInput:checked+.toggleSwitch { + background-color: rgb(148, 118, 255); + transition-duration: .2s; +} + .user-message { align-self: flex-end; - background-color: #012139; + background-color: #0d1820; } .bot-message { align-self: flex-start; - background-color: #121312; + background-color: #0c0c0c; } .chat-item { diff --git a/Frontend/MainFE/wwwroot/css/home.css b/Frontend/MainFE/wwwroot/css/home.css index 5f9375d3..1d23464e 100644 --- a/Frontend/MainFE/wwwroot/css/home.css +++ b/Frontend/MainFE/wwwroot/css/home.css @@ -23,6 +23,53 @@ body { margin-bottom: 20px; } + +@media (max-width: 767px) { + .social-media-buttons { + gap: 5px !important; + opacity: 0; + margin-left: 0 !important; + animation: popup 1s ease-in-out forwards; + animation-delay: 0.5s; + } +} + +.social-media-buttons { + display: flex; + gap: 20px; + position: relative; + opacity: 0; + margin-left: 25%; + animation: popup 1s ease-in-out forwards; + animation-delay: 0.5s; +} + +.button { + text-decoration: none; + color: white; + padding: 15px 30px; + border-radius: 10px; + border-color: wheat; + border-width: 2px; + border-style: solid; + transition: transform 0.3s ease; +} + +.button:hover { + transform: scale(1.1); +} + +@keyframes popup { + 0% { + transform: translateY(50px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} + .fancy-text { font-family: "Tiny5", sans-serif; font-weight: 400; diff --git a/Frontend/MainFE/wwwroot/css/rag.css b/Frontend/MainFE/wwwroot/css/rag.css new file mode 100644 index 00000000..96e149aa --- /dev/null +++ b/Frontend/MainFE/wwwroot/css/rag.css @@ -0,0 +1,360 @@ +/* Body and Layout */ +body { + background-color: #f0f0f0; /* Light grey background */ + color: #333; /* Darker text for contrast */ + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; +} + +/* Header */ +header { + background-color: #ffffff; /* White background for the header */ + color: #333; /* Dark text color */ + padding: 15px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + font-family: 'Tiny5', monospace; /* Coding-style font for the title */ +} + +nav { + display: none !important; +} + +header h1 { + margin: 0; + font-size: 2em; +} + +/* Toggle Switch for Dark Mode */ +.theme-switch-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 10px; +} + +.theme-switch { + display: inline-block; + height: 34px; + position: relative; + width: 60px; +} + +.theme-switch input { + display: none; +} + +.slider { + background-color: #ccc; + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; +} + +.slider:before { + background-color: white; + bottom: 4px; + content: ""; + height: 26px; + left: 4px; + position: absolute; + transition: .4s; + width: 26px; +} + +input:checked + .slider { + background-color: #66bb6a; +} + +input:checked + .slider:before { + transform: translateX(26px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +/* Cards */ +.card-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 30px; +} + +.card { + background-color: #ffffff; /* White background for cards */ + border-radius: 10px; + padding: 20px; + margin: 15px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s; + max-width: 300px; + text-align: center; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.card-content { + text-align: center; +} + +.card-image { + max-width: 100%; + max-height: 150px; + margin-bottom: 15px; + border-radius: 5px; + border: 1px solid #ddd; /* Light grey border around the images */ +} + +/* Buttons */ +.btn-secondary { + background-color: #0078D4; + color: white; + border: none; + border-radius: 5px; + padding: 10px 20px; + cursor: pointer; + transition: background-color 0.3s; +} + +.btn-secondary:hover { + background-color: #005A9E; +} + +/* Modal Styling */ +.custom-modal-dialog { + max-width: 800px; + height: 25rem; +} + +.custom-modal { + background-color: #f0f0f0; + color: #333; +} + +.custom-modal-header { + background-color: #0078D4; + color: #fff; + border-bottom: 1px solid #005A9E; + border-radius: 5px 5px 0 0; +} + +.custom-modal-body { + padding: 20px; +} + +/* Miscellaneous */ +.close { + color: #333; + opacity: 0.8; + cursor: pointer; +} + +.close:hover { + opacity: 1; +} + +/* Footer */ +footer { + background-color: #ffffff; + color: #333; + text-align: center; + padding: 10px 0; + box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.1); + margin-top: 20px; +} + + +.logo-container { + display: flex; + justify-content: center; + width: 100%; +} + +.center-logo { + display: block; + max-height: 70px; +} + +.ask-assistant-button { + position: absolute; + left: 20px; + transform: translateY(-50%); + padding: 10px 20px; + background-color: #0078D4; + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + transition: background-color 0.3s; +} + +.information-button { + position: absolute; + right: 20px; + transform: translateY(-50%); + padding: 10px 20px; + background-color: #0078D4; + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + transition: background-color 0.3s; +} + +.information-button:hover { + background-color: #005A9E; +} + +.ask-assistant-button:hover { + background-color: #005A9E; +} +.cleanup-button { + background-color: #4CAF50; /* Green background */ + border: none; /* Remove borders */ + color: white; /* White text */ + padding: 5px 10px; /* Some padding */ + text-align: center; /* Centered text */ + text-decoration: none; /* Remove underline */ + display: inline-block; /* Get the element to behave like an inline-block */ + font-size: 13px; /* Increase font size */ + cursor: pointer; /* Pointer/hand icon */ + border-radius: 8px; /* Rounded corners */ + transition-duration: 0.4s; /* Smooth transition */ + margin-left: 10px; +} + +.cleanup-button:hover { + background-color: white; /* White background on hover */ + color: black; /* Black text on hover */ + border: 2px solid #4CAF50; /* Green border on hover */ +} + +/* Media Queries */ +@media (max-width: 768px) { + .card-container { + flex-direction: column; + align-items: center; + } + + .card { + max-width: 90%; + margin: 10px 0; + } +} + + +.collapsible { + text-align: left; +} + +.inp { + background: #06495e !important; + color: aliceblue !important; + border-radius: 10px; + border-color: #1c1f23; + padding: 10px; + margin-right: 10px; + flex-grow: 1; +} + + +@media (max-width: 768px) { + .messages-container { + height: 40vh; + } +} + +.messages-container { + flex-grow: 1; + overflow-y: auto; + padding: 10px; + max-height: 90vh; + min-height: 20rem; + display: flex; + border-color: #939393; + border-radius: 15px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + flex-direction: column; + background-color: #dcdcdc !important; +} + +.input-container { + max-height: 7%; + display: flex; + padding: 5px; + position: relative; +} + +.message-card { + margin-bottom: 15px; + width: 80%; + height: fit-content; + padding: 10px; + color: #fff; +} + +.message-card p { + margin: 0; +} + +.user-message { + align-self: flex-end; + background-color: #1c99fa !important; +} + +.bot-message { + align-self: flex-start; + background-color: #5a37ea !important; +} + +@media (min-width: 768px) { + .containerbox { + flex-direction: row; + } +} + +@media (min-width: 768px) { + .input-container { + flex-direction: row; + } +} + +@media (max-width: 767px) { + .input-container { + height: 20vh; + padding: 10px; + } + + .inp { + height: 5vh; + } + + .containerbox { + margin: 0 auto; + } +} + +.message-role-bot { + color: #676767 !important; + align-self: flex-start; +} + +.message-role-user { + color: #676767 !important; + align-self: flex-end; +} + diff --git a/Frontend/MainFE/wwwroot/favicon.png b/Frontend/MainFE/wwwroot/favicon.png index 8422b596..1dbdf565 100644 Binary files a/Frontend/MainFE/wwwroot/favicon.png and b/Frontend/MainFE/wwwroot/favicon.png differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop1.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop1.jpg new file mode 100644 index 00000000..2be30849 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop1.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop2.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop2.jpg new file mode 100644 index 00000000..721dfdc0 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop2.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop3.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop3.jpg new file mode 100644 index 00000000..d110a4bf Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop3.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop4.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop4.jpg new file mode 100644 index 00000000..eab73fe0 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop4.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop5.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop5.jpg new file mode 100644 index 00000000..c4f45dad Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop5.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/laptop6.jpg b/Frontend/MainFE/wwwroot/images/demo/laptop6.jpg new file mode 100644 index 00000000..62f5130d Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/laptop6.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/logo.png b/Frontend/MainFE/wwwroot/images/demo/logo.png new file mode 100644 index 00000000..860a6461 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/logo.png differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc1.jpg b/Frontend/MainFE/wwwroot/images/demo/pc1.jpg new file mode 100644 index 00000000..80bd0925 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc1.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc2.png b/Frontend/MainFE/wwwroot/images/demo/pc2.png new file mode 100644 index 00000000..7fbe25f3 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc2.png differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc3.jpg b/Frontend/MainFE/wwwroot/images/demo/pc3.jpg new file mode 100644 index 00000000..d3f7276e Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc3.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc4.jpg b/Frontend/MainFE/wwwroot/images/demo/pc4.jpg new file mode 100644 index 00000000..060b61ae Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc4.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc5.jpg b/Frontend/MainFE/wwwroot/images/demo/pc5.jpg new file mode 100644 index 00000000..339c3e9b Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc5.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/demo/pc6.jpg b/Frontend/MainFE/wwwroot/images/demo/pc6.jpg new file mode 100644 index 00000000..a1a150e3 Binary files /dev/null and b/Frontend/MainFE/wwwroot/images/demo/pc6.jpg differ diff --git a/Frontend/MainFE/wwwroot/images/robot.png b/Frontend/MainFE/wwwroot/images/robot.png index 37a96768..968c5dec 100644 Binary files a/Frontend/MainFE/wwwroot/images/robot.png and b/Frontend/MainFE/wwwroot/images/robot.png differ diff --git a/Frontend/MainFE/wwwroot/initial_rag_demo.json b/Frontend/MainFE/wwwroot/initial_rag_demo.json new file mode 100644 index 00000000..18f46759 --- /dev/null +++ b/Frontend/MainFE/wwwroot/initial_rag_demo.json @@ -0,0 +1,40 @@ +[ + { + "id": "d2f191c7-f08b-4285-b0d6-bb99a045ebde", + "name": "agent_one", + "description": "This is the first RAG agent", + "model": "gemma2:2b", + "context": + { + "instruction": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product", + "source": + { + "type": 1, + "details": + { + "url": "http://localhost:5098/items", + "method": "Get", + "query": "", + "payload": "" + } + }, + "steps": ["FETCH_DATA*","ANSWER", "REDIRECT+f29211e9-9xe8-45f4-bdbb-054cb835d0d6+AS_Output+REPLACE"], + "relations": + [ + "ac243657-5ab1-4727-b4be-1ea5ae2e76d3", + "f29211e9-9xe8-45f4-bdbb-054cb835d0d6" + ] + } +}, +{ + "id": "f29211e9-9xe8-45f4-bdbb-054cb835d0d6", + "name": "agent_three", + "description": "This is the third RAG agent", + "model": "gemma2:2b", + "context": + { + "instruction": "Adjust previous response to be better for marketing purposes. Dont include any introduction, just pure content", + "steps": ["ANSWER"] + } +} +] \ No newline at end of file diff --git a/Frontend/MainFE/wwwroot/initial_rag_demo_docker.json b/Frontend/MainFE/wwwroot/initial_rag_demo_docker.json new file mode 100644 index 00000000..8d46ef46 --- /dev/null +++ b/Frontend/MainFE/wwwroot/initial_rag_demo_docker.json @@ -0,0 +1,40 @@ +[ + { + "id": "d2f191c7-f08b-4285-b0d6-bb99a045ebde", + "name": "agent_one", + "description": "This is the first RAG agent", + "model": "gemma2:2b", + "context": + { + "instruction": "You are shop assistant in a GeekITStuff store. You have to help the customer in finding the right product", + "source": + { + "type": 1, + "details": + { + "url": "http://demo:8080/items", + "method": "Get", + "query": "", + "payload": "" + } + }, + "steps": ["FETCH_DATA*","ANSWER", "REDIRECT+f29211e9-9xe8-45f4-bdbb-054cb835d0d6+AS_Output+REPLACE"], + "relations": + [ + "ac243657-5ab1-4727-b4be-1ea5ae2e76d3", + "f29211e9-9xe8-45f4-bdbb-054cb835d0d6" + ] + } +}, +{ + "id": "f29211e9-9xe8-45f4-bdbb-054cb835d0d6", + "name": "agent_three", + "description": "This is the third RAG agent", + "model": "gemma2:2b", + "context": + { + "instruction": "Adjust previous response to be better for marketing purposes. Dont include any introduction, just pure content", + "steps": ["ANSWER"] + } +} +] \ No newline at end of file diff --git a/MaIN.sln b/MaIN.sln index dc7c237b..fd019bab 100644 --- a/MaIN.sln +++ b/MaIN.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaIN.Services", "src\MaIN.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaIN.Domain", "src\MaIN.Domain\MaIN.Domain.csproj", "{4DEF4AA6-DFD8-4626-B8BE-2F773B12FE0C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaIN.RAG.Demo", "src\MaIN.RAG.Demo\MaIN.RAG.Demo.csproj", "{2351B015-EA33-42F3-A844-CECABF140E9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {4DEF4AA6-DFD8-4626-B8BE-2F773B12FE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {4DEF4AA6-DFD8-4626-B8BE-2F773B12FE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {4DEF4AA6-DFD8-4626-B8BE-2F773B12FE0C}.Release|Any CPU.Build.0 = Release|Any CPU + {2351B015-EA33-42F3-A844-CECABF140E9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2351B015-EA33-42F3-A844-CECABF140E9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2351B015-EA33-42F3-A844-CECABF140E9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2351B015-EA33-42F3-A844-CECABF140E9A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml index d47f7359..3dede400 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,7 @@ services: - mongo-data:/data/db backend: - build: - context: ./src - dockerfile: ./Dockerfile + image: inza124/main-backend:latest container_name: backend restart: always ports: @@ -23,9 +21,7 @@ services: LocalHost: http://host.docker.internal blazor: - build: - context: ./Frontend/MainFE - dockerfile: ./Dockerfile + image: inza124/main-blazor:latest container_name: blazor restart: always ports: @@ -33,8 +29,21 @@ services: depends_on: - backend environment: - ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_ENVIRONMENT: Docker API_URL: http://backend:8080 + DEMO_API_URL: http://demo:8080 + + demo: + image: inza124/main-demo + container_name: demo + restart: always + ports: + - "5002:8080" + depends_on: + - blazor + environment: + ASPNETCORE_ENVIRONMENT: Docker + LocalHost: http://host.docker.internal volumes: mongo-data: diff --git a/src/Dockerfile.demo b/src/Dockerfile.demo new file mode 100644 index 00000000..4167d4ba --- /dev/null +++ b/src/Dockerfile.demo @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY MaIN.RAG.Demo/*.csproj ./MaIN.RAG.Demo/ + +RUN dotnet restore ./MaIN.RAG.Demo/MaIN.RAG.Demo.csproj + +COPY . ./ +RUN dotnet publish ./MaIN.RAG.Demo/MaIN.RAG.Demo.csproj -c Release -o /app/out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY --from=build /app/out . +ENTRYPOINT ["dotnet", "MaIN.RAG.Demo.dll"] \ No newline at end of file diff --git a/src/MaIN.Domain/Chat.cs b/src/MaIN.Domain/Chat.cs deleted file mode 100644 index 7f72878b..00000000 --- a/src/MaIN.Domain/Chat.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MaIN.Models; - -public class Chat -{ - public string Id { get; set; } - public string Name { get; set; } - public string Model { get; set; } - public List Messages { get; set; } - public bool Stream { get; set; } = false; -} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Agent.cs b/src/MaIN.Domain/Entities/Agents/Agent.cs new file mode 100644 index 00000000..9fe815fa --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Agent.cs @@ -0,0 +1,12 @@ +namespace MaIN.Domain.Entities.Agents; + +public class Agent +{ + public string Id { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public string? Description { get; set; } + public bool Started { get; set; } + public AgentContext Context { get; set; } + public string? ChatId { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentContext.cs b/src/MaIN.Domain/Entities/Agents/AgentContext.cs new file mode 100644 index 00000000..3bc9b216 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentContext.cs @@ -0,0 +1,10 @@ +namespace MaIN.Domain.Entities.Agents; + +public class AgentContext +{ + public string Instruction { get; set; } + public AgentSource.AgentSource? Source { get; set; } + public ILookup Steps { get; set; } + public List? Relations { get; set; } + +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentRelations.cs b/src/MaIN.Domain/Entities/Agents/AgentRelations.cs new file mode 100644 index 00000000..c38972b7 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentRelations.cs @@ -0,0 +1,6 @@ +namespace MaIN.Domain.Entities.Agents; + +public class AgentRelation +{ + public string Id { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentAPISourceDetails.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentAPISourceDetails.cs new file mode 100644 index 00000000..061f585f --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentAPISourceDetails.cs @@ -0,0 +1,9 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentApiSourceDetails : AgentSourceDetailsBase +{ + public string Url { get; set; } + public string Method { get; set; } + public string? Payload { get; set; } + public string? Query { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentFileSourceDetails.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentFileSourceDetails.cs new file mode 100644 index 00000000..e47fbb35 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentFileSourceDetails.cs @@ -0,0 +1,6 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentFileSourceDetails : AgentSourceDetailsBase +{ + public string Path { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentNoSqlSourceDetails.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentNoSqlSourceDetails.cs new file mode 100644 index 00000000..6192138c --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentNoSqlSourceDetails.cs @@ -0,0 +1,9 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentNoSqlSourceDetails +{ + public string ConnectionString { get; set; } + public string DbName { get; set; } + public string Collection { get; set; } + public string Query { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSource.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSource.cs new file mode 100644 index 00000000..a5b03b49 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSource.cs @@ -0,0 +1,7 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentSource +{ + public object? Details { get; set; } + public AgentSourceType Type { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceDetailsBase.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceDetailsBase.cs new file mode 100644 index 00000000..dd4a0354 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceDetailsBase.cs @@ -0,0 +1,6 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentSourceDetailsBase +{ + +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceType.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceType.cs new file mode 100644 index 00000000..281041d0 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSourceType.cs @@ -0,0 +1,10 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public enum AgentSourceType +{ + API = 1, + SQL = 2, + NoSQL = 3, + File = 4, + Text = 5, +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSqlSourceDetails.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSqlSourceDetails.cs new file mode 100644 index 00000000..313643d7 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentSqlSourceDetails.cs @@ -0,0 +1,8 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentSqlSourceDetails +{ + public string ConnectionString { get; set; } + public string Table { get; set; } + public string Query { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/AgentSource/AgentTextSourceDetails.cs b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentTextSourceDetails.cs new file mode 100644 index 00000000..fb050ab3 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/AgentSource/AgentTextSourceDetails.cs @@ -0,0 +1,6 @@ +namespace MaIN.Domain.Entities.Agents.AgentSource; + +public class AgentTextSourceDetails : AgentSourceDetailsBase +{ + public string Text { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Commands/AnswerCommand.cs b/src/MaIN.Domain/Entities/Agents/Commands/AnswerCommand.cs new file mode 100644 index 00000000..61df6c6e --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Commands/AnswerCommand.cs @@ -0,0 +1,8 @@ +using MaIN.Domain.Entities.Agents.Commands.Base; + +namespace MaIN.Domain.Entities.Agents.Commands; + +public class AnswerCommand : BaseCommand +{ + +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Commands/Base/BaseCommand.cs b/src/MaIN.Domain/Entities/Agents/Commands/Base/BaseCommand.cs new file mode 100644 index 00000000..fe32e56a --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Commands/Base/BaseCommand.cs @@ -0,0 +1,6 @@ +namespace MaIN.Domain.Entities.Agents.Commands.Base; + +public class BaseCommand +{ + public Chat? Chat { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Commands/FetchCommand.cs b/src/MaIN.Domain/Entities/Agents/Commands/FetchCommand.cs new file mode 100644 index 00000000..0300b4e1 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Commands/FetchCommand.cs @@ -0,0 +1,9 @@ +using MaIN.Domain.Entities.Agents.Commands.Base; + +namespace MaIN.Domain.Entities.Agents.Commands; + +public class FetchCommand : BaseCommand +{ + public string? Filter { get; set; } + public AgentContext Context { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Commands/RedirectCommand.cs b/src/MaIN.Domain/Entities/Agents/Commands/RedirectCommand.cs new file mode 100644 index 00000000..336d28ec --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Commands/RedirectCommand.cs @@ -0,0 +1,16 @@ +using MaIN.Domain.Entities.Agents.Commands.Base; + +namespace MaIN.Domain.Entities.Agents.Commands; + +public class RedirectCommand : BaseCommand +{ + public Message Message { get; set; } + public string RelatedAgentId { get; set; } = null!; + public OutputTypeOfRedirect SaveAs { get; set; } +} + +public enum OutputTypeOfRedirect +{ + AS_Filter, + AS_Output +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Agents/Commands/StartCommand.cs b/src/MaIN.Domain/Entities/Agents/Commands/StartCommand.cs new file mode 100644 index 00000000..538b3b43 --- /dev/null +++ b/src/MaIN.Domain/Entities/Agents/Commands/StartCommand.cs @@ -0,0 +1,8 @@ +using MaIN.Domain.Entities.Agents.Commands.Base; + +namespace MaIN.Domain.Entities.Agents.Commands; + +public class StartCommand : BaseCommand +{ + public string InitialPrompt { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/Chat.cs b/src/MaIN.Domain/Entities/Chat.cs new file mode 100644 index 00000000..a74e9bd6 --- /dev/null +++ b/src/MaIN.Domain/Entities/Chat.cs @@ -0,0 +1,12 @@ +namespace MaIN.Domain.Entities; + +public class Chat +{ + public string Id { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public List? Messages { get; set; } + public ChatType Type { get; set; } + public bool Stream { get; set; } = false; + public Dictionary Properties { get; set; } = new(); +} \ No newline at end of file diff --git a/src/MaIN.Domain/Entities/ChatType.cs b/src/MaIN.Domain/Entities/ChatType.cs new file mode 100644 index 00000000..9d82f090 --- /dev/null +++ b/src/MaIN.Domain/Entities/ChatType.cs @@ -0,0 +1,7 @@ +namespace MaIN.Domain.Entities; + +public enum ChatType +{ + Conversation = 1, + Rag = 2, +} \ No newline at end of file diff --git a/src/MaIN.Domain/Message.cs b/src/MaIN.Domain/Entities/Message.cs similarity index 62% rename from src/MaIN.Domain/Message.cs rename to src/MaIN.Domain/Entities/Message.cs index 2ddd9c32..5d2d5908 100644 --- a/src/MaIN.Domain/Message.cs +++ b/src/MaIN.Domain/Entities/Message.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace MaIN.Models; +namespace MaIN.Domain.Entities; public class Message { diff --git a/src/MaIN.Domain/MaIN.Domain.csproj b/src/MaIN.Domain/MaIN.Domain.csproj index 45444911..3a635329 100644 --- a/src/MaIN.Domain/MaIN.Domain.csproj +++ b/src/MaIN.Domain/MaIN.Domain.csproj @@ -6,8 +6,4 @@ enable - - - - diff --git a/src/MaIN.Infrastructure/Bootstrapper.cs b/src/MaIN.Infrastructure/Bootstrapper.cs index b728b3f4..280b9ecf 100644 --- a/src/MaIN.Infrastructure/Bootstrapper.cs +++ b/src/MaIN.Infrastructure/Bootstrapper.cs @@ -1,6 +1,6 @@ using MaIN.Infrastructure.Configuration; -using MaIN.Infrastructure.Providers.cs; -using MaIN.Infrastructure.Providers.cs.Abstract; +using MaIN.Infrastructure.Repositories; +using MaIN.Infrastructure.Repositories.Abstract; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; @@ -23,11 +23,18 @@ public static IServiceCollection ConfigureInfrastructure(this IServiceCollection new MongoClient(configuration.GetSection("MongoDbSettings:ConnectionString").Value)); } - services.AddScoped(sp => + services.AddSingleton(sp => { var mongoClient = sp.GetRequiredService(); var database = mongoClient.GetDatabase(configuration.GetSection("MongoDbSettings:DatabaseName").Value); - return new ChatProvider(database, configuration.GetSection("MongoDbSettings:CollectionName").Value!); + return new ChatRepository(database, "Chats"); + }); + + services.AddSingleton(sp => + { + var mongoClient = sp.GetRequiredService(); + var database = mongoClient.GetDatabase(configuration.GetSection("MongoDbSettings:DatabaseName").Value); + return new AgentRepository(database, "Agents"); }); return services; diff --git a/src/MaIN.Infrastructure/Models/AgentContextDocument.cs b/src/MaIN.Infrastructure/Models/AgentContextDocument.cs new file mode 100644 index 00000000..fa4a3853 --- /dev/null +++ b/src/MaIN.Infrastructure/Models/AgentContextDocument.cs @@ -0,0 +1,11 @@ +using MaIN.Models.Rag; + +namespace MaIN.Infrastructure.Models; + +public class AgentContextDocument +{ + public string Instruction { get; set; } + public AgentSourceDocument? Source { get; set; } + public List Steps { get; set; } + public List? Relations { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Models/AgentDocument.cs b/src/MaIN.Infrastructure/Models/AgentDocument.cs new file mode 100644 index 00000000..fd08b8de --- /dev/null +++ b/src/MaIN.Infrastructure/Models/AgentDocument.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace MaIN.Infrastructure.Models; + +public class AgentDocument +{ + [BsonId] + public string Id { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public string? Description { get; set; } + public bool Started { get; set; } + public AgentContextDocument Context { get; set; } + public string ChatId { get; set; } = null!; +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs b/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs new file mode 100644 index 00000000..726dc527 --- /dev/null +++ b/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs @@ -0,0 +1,7 @@ +namespace MaIN.Models.Rag; + +public class AgentSourceDocument +{ + public string? DetailsSerialized { get; set; } + public AgentSourceTypeDocument Type { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs b/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs new file mode 100644 index 00000000..49bb1d99 --- /dev/null +++ b/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs @@ -0,0 +1,10 @@ +namespace MaIN.Models.Rag; + +public enum AgentSourceTypeDocument +{ + API = 1, + SQL = 2, + NoSQL = 3, + File = 4, + Text = 5, +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Models/ChatDocument.cs b/src/MaIN.Infrastructure/Models/ChatDocument.cs index 557b95b8..3737a76d 100644 --- a/src/MaIN.Infrastructure/Models/ChatDocument.cs +++ b/src/MaIN.Infrastructure/Models/ChatDocument.cs @@ -9,5 +9,7 @@ public class ChatDocument public string Name { get; set; } public string Model { get; set; } public List Messages { get; set; } + public ChatTypeDocument Type { get; set; } + public Dictionary Properties { get; set; } public bool Stream { get; set; } = false; } \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Models/ChatTypeDocument.cs b/src/MaIN.Infrastructure/Models/ChatTypeDocument.cs new file mode 100644 index 00000000..fd490051 --- /dev/null +++ b/src/MaIN.Infrastructure/Models/ChatTypeDocument.cs @@ -0,0 +1,7 @@ +namespace MaIN.Infrastructure.Models; + +public enum ChatTypeDocument +{ + Conversation = 1, + Rag = 2, +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Providers.cs/ChatProvider.cs b/src/MaIN.Infrastructure/Providers.cs/ChatProvider.cs deleted file mode 100644 index 8d00e377..00000000 --- a/src/MaIN.Infrastructure/Providers.cs/ChatProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MaIN.Infrastructure.Models; -using MaIN.Infrastructure.Providers.cs.Abstract; -using MongoDB.Driver; - -namespace MaIN.Infrastructure.Providers.cs; - -public class ChatProvider(IMongoDatabase database, string collectionName) : IChatProvider -{ - private readonly IMongoCollection _chats = database.GetCollection(collectionName); - - public async Task> GetAllChats() - { - return await _chats.Find(chat => true).ToListAsync(); - } - - public async Task GetChatById(string id) - { - return await _chats.Find(chat => chat.Id == id).FirstOrDefaultAsync(); - } - - public async Task AddChat(ChatDocument chat) - { - await _chats.InsertOneAsync(chat); - } - - public async Task UpdateChat(string id, ChatDocument chat) - { - await _chats.ReplaceOneAsync(x => x.Id == id, chat); - } - - public async Task DeleteChat(string id) - { - await _chats.DeleteOneAsync(x => x.Id == id); - } -} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Repositories/Abstract/IAgentRepository.cs b/src/MaIN.Infrastructure/Repositories/Abstract/IAgentRepository.cs new file mode 100644 index 00000000..f6bff715 --- /dev/null +++ b/src/MaIN.Infrastructure/Repositories/Abstract/IAgentRepository.cs @@ -0,0 +1,13 @@ +using MaIN.Infrastructure.Models; + +namespace MaIN.Infrastructure.Repositories.Abstract; + +public interface IAgentRepository +{ + Task> GetAllAgents(); + Task GetAgentById(string id); + Task AddAgent(AgentDocument? agent); + Task UpdateAgent(string id, AgentDocument? agent); + Task DeleteAgent(string id); + Task Exists(string id); +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Providers.cs/Abstract/IChatProvider.cs b/src/MaIN.Infrastructure/Repositories/Abstract/IChatRepository.cs similarity index 74% rename from src/MaIN.Infrastructure/Providers.cs/Abstract/IChatProvider.cs rename to src/MaIN.Infrastructure/Repositories/Abstract/IChatRepository.cs index b4205359..0b9cd966 100644 --- a/src/MaIN.Infrastructure/Providers.cs/Abstract/IChatProvider.cs +++ b/src/MaIN.Infrastructure/Repositories/Abstract/IChatRepository.cs @@ -1,8 +1,8 @@ using MaIN.Infrastructure.Models; -namespace MaIN.Infrastructure.Providers.cs.Abstract; +namespace MaIN.Infrastructure.Repositories.Abstract; -public interface IChatProvider +public interface IChatRepository { Task> GetAllChats(); Task GetChatById(string id); diff --git a/src/MaIN.Infrastructure/Repositories/AgentRepository.cs b/src/MaIN.Infrastructure/Repositories/AgentRepository.cs new file mode 100644 index 00000000..b4d9302b --- /dev/null +++ b/src/MaIN.Infrastructure/Repositories/AgentRepository.cs @@ -0,0 +1,29 @@ +using MaIN.Infrastructure.Models; +using MaIN.Infrastructure.Repositories.Abstract; +using MongoDB.Driver; + +namespace MaIN.Infrastructure.Repositories; + +public class AgentRepository(IMongoDatabase database, string collectionName) : IAgentRepository +{ + private readonly IMongoCollection _agents = database.GetCollection(collectionName)!; + + public async Task> GetAllAgents() => + await _agents.Find(chat => true).ToListAsync(); + + public async Task GetAgentById(string id) => + await _agents!.Find(agent => agent.Id == id).FirstOrDefaultAsync(); + + public async Task AddAgent(AgentDocument? agent) => + await _agents.InsertOneAsync(agent); + + public async Task UpdateAgent(string id, AgentDocument? agent) => + await _agents.ReplaceOneAsync(x => x.Id == id, agent); + + public async Task DeleteAgent(string id) => + await _agents.DeleteOneAsync(x => x.Id == id); + + public async Task Exists(string id) => + (await _agents.CountDocumentsAsync(x => x!.Id == id)) > 0; + +} \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Repositories/ChatRepository.cs b/src/MaIN.Infrastructure/Repositories/ChatRepository.cs new file mode 100644 index 00000000..aaa3ddd3 --- /dev/null +++ b/src/MaIN.Infrastructure/Repositories/ChatRepository.cs @@ -0,0 +1,25 @@ +using MaIN.Infrastructure.Models; +using MaIN.Infrastructure.Repositories.Abstract; +using MongoDB.Driver; + +namespace MaIN.Infrastructure.Repositories; + +public class ChatRepository(IMongoDatabase database, string collectionName) : IChatRepository +{ + private readonly IMongoCollection _chats = database.GetCollection(collectionName); + + public async Task> GetAllChats() => + await _chats.Find(chat => true).ToListAsync(); + + public async Task GetChatById(string id) => + await _chats.Find(chat => chat.Id == id).FirstOrDefaultAsync(); + + public async Task AddChat(ChatDocument chat) => + await _chats.InsertOneAsync(chat); + + public async Task UpdateChat(string id, ChatDocument chat) => + await _chats.ReplaceOneAsync(x => x.Id == id, chat); + + public async Task DeleteChat(string id) => + await _chats.DeleteOneAsync(x => x.Id == id); +} \ No newline at end of file diff --git a/src/MaIN.RAG.Demo/MaIN.RAG.Demo.csproj b/src/MaIN.RAG.Demo/MaIN.RAG.Demo.csproj new file mode 100644 index 00000000..37dc7448 --- /dev/null +++ b/src/MaIN.RAG.Demo/MaIN.RAG.Demo.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + Always + + + + diff --git a/src/MaIN.RAG.Demo/MaIN.RAG.Demo.http b/src/MaIN.RAG.Demo/MaIN.RAG.Demo.http new file mode 100644 index 00000000..fdce86c5 --- /dev/null +++ b/src/MaIN.RAG.Demo/MaIN.RAG.Demo.http @@ -0,0 +1,6 @@ +@MaIN.RAG.Demo_HostAddress = http://localhost:5098 + +GET {{MaIN.RAG.Demo_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/MaIN.RAG.Demo/Program.cs b/src/MaIN.RAG.Demo/Program.cs new file mode 100644 index 00000000..5972f42d --- /dev/null +++ b/src/MaIN.RAG.Demo/Program.cs @@ -0,0 +1,51 @@ +using System.Text.Json; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var serializeOptions = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true +}; + + +app.MapGet("/items/", () => Results.Ok( + JsonSerializer.Deserialize>( + File.ReadAllText("json/items.json"), serializeOptions))) + .WithName("GetHardwareItems") + .WithOpenApi(); + +app.Run(); + + +public class Hardware +{ + public int Id { get; set; } + public string Image { get; set; } + public string Name { get; set; } + public string Brand { get; set; } + public string Processor { get; set; } + public string Type { get; set; } + public string Ram { get; set; } + public string Storage { get; set; } + public string Gpu { get; set; } + public double Price { get; set; } + public string Availability { get; set; } + public string Description { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.RAG.Demo/Properties/launchSettings.json b/src/MaIN.RAG.Demo/Properties/launchSettings.json new file mode 100644 index 00000000..75df4c08 --- /dev/null +++ b/src/MaIN.RAG.Demo/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54552", + "sslPort": 44303 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5098", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7205;http://localhost:5098", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/MaIN.RAG.Demo/appsettings.Development.json b/src/MaIN.RAG.Demo/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/src/MaIN.RAG.Demo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/MaIN.RAG.Demo/appsettings.json b/src/MaIN.RAG.Demo/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/src/MaIN.RAG.Demo/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/MaIN.RAG.Demo/json/items.json b/src/MaIN.RAG.Demo/json/items.json new file mode 100644 index 00000000..a221d646 --- /dev/null +++ b/src/MaIN.RAG.Demo/json/items.json @@ -0,0 +1,170 @@ +[ + { + "id": 1, + "image": "pc1.jpg", + "type": "PC", + "name": "Gaming PC Ultra", + "brand": "PowerTech", + "processor": "Intel Core i9-11900K", + "ram": "32GB DDR4", + "storage": "1TB SSD + 2TB HDD", + "gpu": "NVIDIA GeForce RTX 3090", + "price": 2999.99, + "availability": "In Stock", + "description": "The Gaming PC Ultra from PowerTech features an Intel Core i9 processor and NVIDIA RTX 3090, making it perfect for high-end gaming and demanding applications. With 32GB of RAM and ample storage, this PC can handle any task with ease." + }, + { + "id": 2, + "image": "pc2.png", + "type": "PC", + "name": "Workstation Pro", + "brand": "TechMaster", + "processor": "AMD Ryzen 9 5950X", + "ram": "64GB DDR4", + "storage": "2TB SSD", + "gpu": "NVIDIA Quadro RTX 5000", + "price": 3499.99, + "availability": "In Stock", + "description": "TechMaster's Workstation Pro is designed for professionals who require top-tier performance. With an AMD Ryzen 9 CPU, 64GB RAM, and NVIDIA Quadro RTX GPU, this workstation excels in 3D rendering, video editing, and other intensive tasks." + }, + { + "id": 3, + "image": "pc3.jpg", + "type": "PC", + "name": "Budget Gamer", + "brand": "EconoPC", + "processor": "Intel Core i5-10400F", + "ram": "16GB DDR4", + "storage": "512GB SSD", + "gpu": "NVIDIA GeForce GTX 1660 Super", + "price": 799.99, + "availability": "Out of Stock", + "description": "The Budget Gamer by EconoPC provides excellent value for entry-level gaming. Featuring an Intel Core i5 processor and NVIDIA GTX 1660 Super GPU, this PC delivers smooth performance for popular games at an affordable price." + }, + { + "id": 4, + "image": "pc4.jpg", + "type": "PC", + "name": "All-Purpose PC", + "brand": "ValueComp", + "processor": "AMD Ryzen 5 3600", + "ram": "16GB DDR4", + "storage": "1TB HDD", + "gpu": "AMD Radeon RX 5700", + "price": 999.99, + "availability": "In Stock", + "description": "ValueComp's All-Purpose PC is a versatile system suitable for gaming, work, and everyday use. It features an AMD Ryzen 5 processor, 16GB RAM, and a Radeon RX 5700 GPU, ensuring balanced performance for a variety of tasks." + }, + { + "id": 5, + "image": "pc5.jpg", + "type": "PC", + "name": "Compact Office PC", + "brand": "OfficeMate", + "processor": "Intel Core i3-10100", + "ram": "8GB DDR4", + "storage": "256GB SSD", + "gpu": "Integrated Graphics", + "price": 499.99, + "availability": "In Stock", + "description": "The Compact Office PC by OfficeMate is ideal for small spaces and routine office tasks. Equipped with an Intel Core i3 processor and 8GB of RAM, it offers reliable performance for word processing, spreadsheets, and internet browsing." + }, + { + "id": 6, + "image": "pc6.jpg", + "type": "PC", + "name": "Performance Beast", + "brand": "ExtremeTech", + "processor": "Intel Core i7-12700K", + "ram": "32GB DDR5", + "storage": "2TB SSD", + "gpu": "NVIDIA GeForce RTX 3080", + "price": 2599.99, + "availability": "In Stock", + "description": "ExtremeTech's Performance Beast is designed for the most demanding users. With an Intel Core i7 processor, 32GB DDR5 RAM, and NVIDIA RTX 3080 GPU, this PC is ideal for gaming, 3D rendering, and other high-performance applications." + }, + { + "id": 7, + "image": "laptop1.jpg", + "type": "Laptop", + "name": "UltraBook Pro", + "brand": "PowerTech", + "processor": "Intel Core i7-1165G7", + "ram": "16GB LPDDR4x", + "storage": "512GB SSD", + "gpu": "Intel Iris Xe Graphics", + "price": 1299.99, + "availability": "In Stock", + "description": "The UltraBook Pro by PowerTech offers a sleek design with powerful performance, featuring an Intel Core i7 processor and Intel Iris Xe Graphics. With 16GB of RAM and a 512GB SSD, this laptop is perfect for professionals on the go." + }, + { + "id": 8, + "image": "laptop2.jpg", + "type": "Laptop", + "name": "Gaming Laptop X", + "brand": "TechMaster", + "processor": "AMD Ryzen 7 5800H", + "ram": "32GB DDR4", + "storage": "1TB SSD", + "gpu": "NVIDIA GeForce RTX 3070", + "price": 1999.99, + "availability": "In Stock", + "description": "TechMaster's Gaming Laptop X is designed for gamers who need high performance on the go. Featuring an AMD Ryzen 7 CPU and NVIDIA RTX 3070 GPU, this laptop delivers smooth gaming experiences and fast load times." + }, + { + "id": 9, + "image": "laptop3.jpg", + "type": "Laptop", + "name": "Budget Laptop", + "brand": "EconoPC", + "processor": "Intel Core i3-1115G4", + "ram": "8GB DDR4", + "storage": "256GB SSD", + "gpu": "Integrated Graphics", + "price": 499.99, + "availability": "Out of Stock", + "description": "The Budget Laptop by EconoPC offers essential features at an affordable price. With an Intel Core i3 processor and 8GB of RAM, it's suitable for students and casual users who need reliable performance for everyday tasks." + }, + { + "id": 10, + "image": "laptop4.jpg", + "type": "Laptop", + "name": "All-Purpose Laptop", + "brand": "ValueComp", + "processor": "AMD Ryzen 5 4500U", + "ram": "16GB DDR4", + "storage": "512GB SSD", + "gpu": "AMD Radeon Graphics", + "price": 899.99, + "availability": "In Stock", + "description": "ValueComp's All-Purpose Laptop is versatile and powerful, equipped with an AMD Ryzen 5 processor and Radeon Graphics. It's ideal for a variety of tasks, including work, entertainment, and light gaming." + }, + { + "id": 11, + "image": "laptop5.jpg", + "type": "Laptop", + "name": "Compact Office Laptop", + "brand": "OfficeMate", + "processor": "Intel Core i5-1135G7", + "ram": "8GB LPDDR4x", + "storage": "512GB SSD", + "gpu": "Intel Iris Xe Graphics", + "price": 799.99, + "availability": "In Stock", + "description": "The Compact Office Laptop by OfficeMate is perfect for professionals who need a portable and efficient device. With an Intel Core i5 processor and 8GB of RAM, it offers solid performance for office applications and multitasking." + }, + { + "id": 12, + "image": "laptop6.jpg", + "type": "Laptop", + "name": "Creative Laptop", + "brand": "CreativeTech", + "processor": "Apple M1", + "ram": "16GB Unified Memory", + "storage": "1TB SSD", + "gpu": "Apple M1 GPU", + "price": 1499.99, + "availability": "In Stock", + "description": "CreativeTech's Creative Laptop is perfect for designers and content creators. Featuring the Apple M1 chip, 16GB of unified memory, and a 1TB SSD, this laptop offers exceptional performance and efficiency for creative tasks." + } +] diff --git a/src/MaIN.Services/Bootstrapper.cs b/src/MaIN.Services/Bootstrapper.cs index e378ef01..8966f844 100644 --- a/src/MaIN.Services/Bootstrapper.cs +++ b/src/MaIN.Services/Bootstrapper.cs @@ -8,8 +8,11 @@ public static class Bootstrapper { public static IServiceCollection ConfigureApplication(this IServiceCollection serviceCollection) { - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); + //TODO solve this with separate registration for actions purposes + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); return serviceCollection; } diff --git a/src/MaIN.Services/MaIN.Services.csproj b/src/MaIN.Services/MaIN.Services.csproj index 25e44223..baa31608 100644 --- a/src/MaIN.Services/MaIN.Services.csproj +++ b/src/MaIN.Services/MaIN.Services.csproj @@ -14,6 +14,7 @@ + diff --git a/src/MaIN.Services/Mappers/AgentMapper.cs b/src/MaIN.Services/Mappers/AgentMapper.cs new file mode 100644 index 00000000..6a23a0a7 --- /dev/null +++ b/src/MaIN.Services/Mappers/AgentMapper.cs @@ -0,0 +1,119 @@ +using System.Text.Json; +using MaIN.Domain.Entities.Agents; +using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Infrastructure.Models; +using MaIN.Models.Rag; +using MaIN.Services.Steps; + +namespace MaIN.Services.Mappers; + +public static class AgentMapper +{ + public static AgentDto ToDto(this Agent? agent) + => new() + { + Id = agent.Id, + Name = agent.Name, + Model = agent.Model, + Started = agent.Started, + Description = agent.Description, + Context = agent.Context.ToDto() + }; + + public static AgentContextDto ToDto(this AgentContext agentContext) + => new() + { + Instruction = agentContext.Instruction, + Relations = agentContext.Relations, + Steps = new List(), + Source = new AgentSourceDto() + { + Details = agentContext.Source.Details, + Type = Enum.Parse(agentContext.Source.Type.ToString()) + } + }; + + public static Agent ToDomain(this AgentDto agent) + => new() + { + Id = agent.Id, + Name = agent.Name, + Model = agent.Model, + Started = agent.Started, + Description = agent.Description, + Context = agent.Context.ToDomain() + }; + + public static AgentContext ToDomain(this AgentContextDto agentContextDto) + => new() + { + Instruction = agentContextDto.Instruction, + Relations = agentContextDto?.Relations, + Source = agentContextDto?.Source is not null ? new AgentSource() + { + Details = agentContextDto?.Source?.Details, + Type = Enum.Parse(agentContextDto?.Source?.Type.ToString()!) + } : null, + Steps = agentContextDto!.Steps.ToLookup(x => x, y => Actions.Steps[y.Split('+').First()]) + }; + + private static AgentSourceDetailsBase MapDetailsToType(object? details, AgentSourceTypeDto? sourceDetailsType) + { + return sourceDetailsType switch + { + AgentSourceTypeDto.Text => (AgentTextSourceDetails)details!, + AgentSourceTypeDto.File => (AgentFileSourceDetails)details!, + AgentSourceTypeDto.API => (AgentApiSourceDetails)details!, + //TBD add all types + _ => new() + }; + } + + public static AgentContextDocument ToDocument(this AgentContext context) + => new() + { + Instruction = context.Instruction, + Relations = context.Relations?.ToList(), + Steps = context.Steps.Select(x => x.Key).ToList(), + Source = context.Source is not null ? new AgentSourceDocument() + { + DetailsSerialized = JsonSerializer.Serialize(context.Source.Details), + Type = Enum.Parse(context.Source.Type.ToString()) + } : null + }; + + public static AgentDocument? ToDocument(this Agent agent) + => new() + { + Id = agent.Id, + Name = agent.Name, + Model = agent.Model, + Started = agent.Started, + Description = agent.Description, + Context = agent.Context.ToDocument() + }; + + public static Agent? ToDomain(this AgentDocument? agent) + => new() + { + Id = agent.Id, + Name = agent.Name, + Model = agent.Model, + Started = agent.Started, + Description = agent.Description, + Context = agent.Context.ToDomain() + }; + + public static AgentContext ToDomain(this AgentContextDocument agentContextDocument) + => new() + { + Instruction = agentContextDocument.Instruction, + Relations = agentContextDocument?.Relations, + Source = new AgentSource + { + Details = agentContextDocument?.Source?.DetailsSerialized, + Type = Enum.Parse(agentContextDocument?.Source?.Type.ToString() ?? AgentSourceType.Text.ToString()) + }, + Steps = agentContextDocument!.Steps.ToLookup(x => x, y => Actions.Steps[y.Split('+').First()]) + }; +} \ No newline at end of file diff --git a/src/MaIN.Services/Mappers/ChatMapper.cs b/src/MaIN.Services/Mappers/ChatMapper.cs index f94bde47..30c87090 100644 --- a/src/MaIN.Services/Mappers/ChatMapper.cs +++ b/src/MaIN.Services/Mappers/ChatMapper.cs @@ -1,3 +1,4 @@ +using MaIN.Domain.Entities; using MaIN.Infrastructure.Models; using MaIN.Models; using MaIN.Services.Models; @@ -13,7 +14,9 @@ public static ChatDto ToDto(this Chat chat) Name = chat.Name, Model = chat.Model, Messages = chat.Messages.Select(m => m.ToDto()).ToList(), - Stream = chat.Stream + Stream = chat.Stream, + Type = Enum.Parse(chat.Type.ToString()), + Properties = chat.Properties }; public static MessageDto ToDto(this Message message) @@ -23,14 +26,16 @@ public static MessageDto ToDto(this Message message) Role = message.Role }; - public static Chat ToDomain(this ChatDto chat) + public static Chat? ToDomain(this ChatDto chat) => new Chat() { Id = chat.Id, Name = chat.Name, Model = chat.Model, - Messages = chat.Messages.Select(m => m.ToDomain()).ToList(), - Stream = chat.Stream + Messages = chat.Messages?.Select(m => m.ToDomain()).ToList(), + Stream = chat.Stream, + Type = Enum.Parse(chat.Type.ToString()), + Properties = chat.Properties }; public static Message ToDomain(this MessageDto message) @@ -47,14 +52,16 @@ public static MessageDocument ToDocument(this Message message) Role = message.Role }; - public static ChatDocument ToDocument(this Chat chat) + public static ChatDocument ToDocument(this Chat? chat) => new ChatDocument() { Id = chat.Id, Name = chat.Name, Model = chat.Model, Messages = chat.Messages.Select(m => m.ToDocument()).ToList(), - Stream = chat.Stream + Properties = chat.Properties, + Stream = chat.Stream, + Type = Enum.Parse(chat.Type.ToString()) }; public static Chat ToDomain(this ChatDocument chat) @@ -64,7 +71,9 @@ public static Chat ToDomain(this ChatDocument chat) Name = chat.Name, Model = chat.Model, Messages = chat.Messages.Select(m => m.ToDomain()).ToList(), - Stream = chat.Stream + Stream = chat.Stream, + Properties = chat.Properties, + Type = Enum.Parse(chat.Type.ToString()) }; public static Message ToDomain(this MessageDocument message) diff --git a/src/MaIN.Services/Models/ChatDto.cs b/src/MaIN.Services/Models/ChatDto.cs index cb62e2b0..cc794c4e 100644 --- a/src/MaIN.Services/Models/ChatDto.cs +++ b/src/MaIN.Services/Models/ChatDto.cs @@ -11,8 +11,13 @@ public class ChatDto [JsonPropertyName("model")] public string Model { get; set; } [JsonPropertyName("messages")] - public List Messages { get; set; } + public List? Messages { get; set; } + [JsonPropertyName("type")] + public ChatTypeDto Type { get; set; } [JsonPropertyName("stream")] public bool Stream { get; set; } = false; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } } \ No newline at end of file diff --git a/src/MaIN.Services/Models/ChatTypeDto.cs b/src/MaIN.Services/Models/ChatTypeDto.cs new file mode 100644 index 00000000..37d98918 --- /dev/null +++ b/src/MaIN.Services/Models/ChatTypeDto.cs @@ -0,0 +1,7 @@ +namespace MaIN.Services.Models; + +public enum ChatTypeDto +{ + Conversation = 1, + Rag = 2, +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentContextDto.cs b/src/MaIN.Services/Models/Rag/AgentContextDto.cs new file mode 100644 index 00000000..0fe64a6b --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentContextDto.cs @@ -0,0 +1,10 @@ +namespace MaIN.Models.Rag; + +public class AgentContextDto +{ + public string Instruction { get; set; } + public AgentSourceDto Source { get; set; } + public List Steps { get; set; } + public List? Relations { get; set; } + +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentDto.cs b/src/MaIN.Services/Models/Rag/AgentDto.cs new file mode 100644 index 00000000..7f255fb4 --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentDto.cs @@ -0,0 +1,11 @@ +namespace MaIN.Models.Rag; + +public class AgentDto +{ + public string Id { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public string? Description { get; set; } + public bool Started { get; set; } + public AgentContextDto Context { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentRelationDto.cs b/src/MaIN.Services/Models/Rag/AgentRelationDto.cs new file mode 100644 index 00000000..2c75cb4c --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentRelationDto.cs @@ -0,0 +1,7 @@ +namespace MaIN.Models.Rag; + +public class AgentRelationDto +{ + public string Id { get; set; } + public string AgentPurpose { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentSource/AgentApiSourceDetailsDto.cs b/src/MaIN.Services/Models/Rag/AgentSource/AgentApiSourceDetailsDto.cs new file mode 100644 index 00000000..4e18d58c --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentSource/AgentApiSourceDetailsDto.cs @@ -0,0 +1,11 @@ +using MaIN.Domain.Entities.Agents.AgentSource; + +namespace MaIN.Models.Rag; + +public class AgentApiSourceDetailsDto : AgentSourceDetailsBase +{ + public string Url { get; set; } + public string Method { get; set; } + public string? Payload { get; set; } + public string? Query { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentSource/AgentFileSourceDetailsDto.cs b/src/MaIN.Services/Models/Rag/AgentSource/AgentFileSourceDetailsDto.cs new file mode 100644 index 00000000..8f672c1b --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentSource/AgentFileSourceDetailsDto.cs @@ -0,0 +1,8 @@ +using MaIN.Domain.Entities.Agents.AgentSource; + +namespace MaIN.Models.Rag; + +public class AgentFileSourceDetailsDto : AgentSourceDetailsBase +{ + public string Path { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceDto.cs b/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceDto.cs new file mode 100644 index 00000000..e6414bbb --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceDto.cs @@ -0,0 +1,7 @@ +namespace MaIN.Models.Rag; + +public class AgentSourceDto +{ + public object? Details { get; set; } + public AgentSourceTypeDto Type { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceTypeDto.cs b/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceTypeDto.cs new file mode 100644 index 00000000..09eb5062 --- /dev/null +++ b/src/MaIN.Services/Models/Rag/AgentSource/AgentSourceTypeDto.cs @@ -0,0 +1,10 @@ +namespace MaIN.Models.Rag; + +public enum AgentSourceTypeDto +{ + API = 1, + SQL = 2, + NoSQL = 3, + File = 4, + Text = 5, +} \ No newline at end of file diff --git a/src/MaIN.Services/Services/Abstract/IAgentService.cs b/src/MaIN.Services/Services/Abstract/IAgentService.cs new file mode 100644 index 00000000..134176f5 --- /dev/null +++ b/src/MaIN.Services/Services/Abstract/IAgentService.cs @@ -0,0 +1,16 @@ +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents; + +namespace MaIN.Services.Services.Abstract; + +public interface IAgentService +{ + Task Process(Chat? chat, string agentId, bool translatePrompt = false); + Task CreateAgent(Agent agent); + Task GetChatByAgent(string agentId); + Task Restart(string agentId); + Task> GetAgents(); + Task GetAgentById(string id); + Task DeleteAgent(string id); + Task AgentExists(string id); +} \ No newline at end of file diff --git a/src/MaIN.Services/Services/Abstract/IChatService.cs b/src/MaIN.Services/Services/Abstract/IChatService.cs index 8600acc3..8f15186d 100644 --- a/src/MaIN.Services/Services/Abstract/IChatService.cs +++ b/src/MaIN.Services/Services/Abstract/IChatService.cs @@ -1,3 +1,4 @@ +using MaIN.Domain.Entities; using MaIN.Models; using MaIN.Services.Models; using MaIN.Services.Models.Ollama; @@ -6,10 +7,9 @@ namespace MaIN.Services.Services.Abstract; public interface IChatService { - Task Create(Chat chat); - Task Completions(Chat chat); + Task Create(Chat? chat); + Task Completions(Chat? chat, bool translatePrompt = false); Task Delete(string id); Task GetById(string id); - Task> GetCurrentModels(); Task> GetAll(); } \ No newline at end of file diff --git a/src/MaIN.Services/Services/Abstract/IOllamaService.cs b/src/MaIN.Services/Services/Abstract/IOllamaService.cs new file mode 100644 index 00000000..90435fa7 --- /dev/null +++ b/src/MaIN.Services/Services/Abstract/IOllamaService.cs @@ -0,0 +1,12 @@ +using MaIN.Domain.Entities; +using MaIN.Models; +using MaIN.Services.Models; +using MaIN.Services.Models.Ollama; + +namespace MaIN.Services.Services.Abstract; + +public interface IOllamaService +{ + Task Send(Chat? chat); + Task> GetCurrentModels(); +} \ No newline at end of file diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs new file mode 100644 index 00000000..867a1e81 --- /dev/null +++ b/src/MaIN.Services/Services/AgentService.cs @@ -0,0 +1,210 @@ +using Amazon.Runtime.Internal.Transform; +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents; +using MaIN.Domain.Entities.Agents.Commands; +using MaIN.Infrastructure.Models; +using MaIN.Infrastructure.Repositories.Abstract; +using MaIN.Models; +using MaIN.Models.Rag; +using MaIN.Services.Mappers; +using MaIN.Services.Models.Ollama; +using MaIN.Services.Services.Abstract; +using MaIN.Services.Steps; +using MongoDB.Driver.Core.Events; + +namespace MaIN.Services.Services; + +public class AgentService( + IAgentRepository agentRepository, + IChatRepository chatRepository) : IAgentService +{ + public async Task Process(Chat? chat, string agentId, bool translatePrompt = false) + { + // Fetch the agent details from the repository + var agent = await agentRepository.GetAgentById(agentId); + + // Ensure the agent and its context are valid + if (agent == null) + { + throw new ArgumentException("Agent not found."); + } + + var context = agent.Context; + if (context == null) + { + throw new ArgumentException("Agent context not found."); + } + + chat = await ProcessSteps(context, chat); + + // Return the processed chat + return chat; + } + + private static async Task ProcessSteps(AgentContextDocument context, Chat? chat) + { + // Process each step in the defined order + foreach (var step in context.Steps) + { + // provide arguments + var stepParts = step.Split('+'); + var stepName = stepParts[0]; + var shouldReplaceLastMessage = step.Contains("REPLACE"); + + // Create the appropriate command based on the step name + switch (stepName) + { + case "REDIRECT": + var redirectCommand = new RedirectCommand + { + Message = chat?.Messages?.Last()!, + RelatedAgentId = stepParts[1], + SaveAs = Enum.Parse(stepParts[2]) + }; + + var message = await Actions.CallAsync("REDIRECT", redirectCommand) as Message; + if (redirectCommand.SaveAs == OutputTypeOfRedirect.AS_Filter) + { + chat?.Properties.TryAdd("data_filter", message!.Content); + } + else + { + if (shouldReplaceLastMessage) + { + chat?.Messages?.RemoveAt(chat.Messages.Count - 1); + } + + chat?.Messages?.Add(message!); + } + + break; + + case "FETCH_DATA": + var filterExist = + chat!.Properties.TryGetValue("data_filter", + out var filter); //TODO define a way to create multiple filters + var fetchCommand = new FetchCommand + { + Chat = chat, + Filter = filterExist ? filter : string.Empty, + Context = context.ToDomain() + }; + var fetchCommandResponse = + (await Actions.CallAsync("FETCH_DATA_WITH_FILTER", fetchCommand) as Message)!; + chat.Messages?.Add(fetchCommandResponse); + break; + + case "FETCH_DATA*": + if (chat!.Properties.ContainsKey("FETCH_DATA*")) + { + break; + } + + var filterExists = + chat!.Properties.TryGetValue("data_filter", + out var filterData); //TODO define a way to create multiple filters + var fetchCommandOnce = new FetchCommand + { + Context = context.ToDomain(), + Chat = chat, + Filter = filterExists ? filterData : string.Empty + }; + var response = (await Actions.CallAsync("FETCH_DATA", fetchCommandOnce) as Message)!; + chat.Messages?.Add(response); + + chat.Properties.Add("FETCH_DATA*", string.Empty); + break; + + case "ANSWER": + var answerCommand = new AnswerCommand + { + Chat = chat + }; + var answerResponse = (await Actions.CallAsync("ANSWER", answerCommand) as Message)!; + + if (shouldReplaceLastMessage) + { + chat?.Messages?.RemoveAt(chat.Messages.Count - 1); + } + + chat?.Messages?.Add(answerResponse); + break; + + default: + throw new InvalidOperationException($"Unknown step: {stepName}"); + } + } + + return chat; + } + + public async Task CreateAgent(Agent agent) + { + var chat = new Chat() + { + Id = Guid.NewGuid().ToString(), + Model = agent.Model, + Name = agent.Name, + Stream = false, + Messages = new List(), + Type = ChatType.Rag, + }; + + var startCommand = new StartCommand() + { + Chat = chat, + InitialPrompt = agent.Context.Instruction, + }; + + var result = await Actions.CallAsync("START", startCommand) as Message; + result!.Role = "system"; + agent.Started = true; + //chat.Messages.Add(result!); + var agentDocument = agent.ToDocument(); + agentDocument!.ChatId = chat.Id; + await chatRepository.AddChat(chat!.ToDocument()); + await agentRepository.AddAgent(agentDocument); + return agent; + } + + public async Task GetChatByAgent(string agentId) + { + var agent = await agentRepository.GetAgentById(agentId); + var chat = await chatRepository.GetChatById(agent.ChatId); + return chat.ToDomain(); + } + + public async Task Restart(string agentId) + { + var agent = await agentRepository.GetAgentById(agentId); + var chat = await chatRepository.GetChatById(agent?.ChatId!); + chat.Messages = chat.Messages.Take(1).ToList(); //Takes only system message and initial prompt + await chatRepository.UpdateChat(chat.Id, chat); + + return chat.ToDomain(); + } + + public async Task> GetAgents() + { + var result = await agentRepository.GetAllAgents(); + return result + .Select(x => x.ToDomain()) + .ToList()!; + } + + public async Task GetAgentById(string id) + { + var result = await agentRepository.GetAgentById(id); + return result?.ToDomain(); + } + + public async Task DeleteAgent(string id) + { + var chat = await GetChatByAgent(id); + await chatRepository.DeleteChat(chat.Id); + await agentRepository.DeleteAgent(id); + } + + public Task AgentExists(string id) => + agentRepository.Exists(id); +} \ No newline at end of file diff --git a/src/MaIN.Services/Services/ChatService.cs b/src/MaIN.Services/Services/ChatService.cs index 988d7277..cd5f9196 100644 --- a/src/MaIN.Services/Services/ChatService.cs +++ b/src/MaIN.Services/Services/ChatService.cs @@ -1,6 +1,7 @@ using System.Text.Json; +using MaIN.Domain.Entities; using MaIN.Infrastructure.Models; -using MaIN.Infrastructure.Providers.cs.Abstract; +using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Models; using MaIN.Services.Mappers; using MaIN.Services.Models; @@ -11,50 +12,41 @@ namespace MaIN.Services.Services; public class ChatService( ITranslatorService translatorService, - IChatProvider chatProvider, + IChatRepository chatProvider, + IOllamaService ollamaService, IHttpClientFactory httpClientFactory) : IChatService { - public async Task Create(Chat chat) - => await chatProvider.AddChat(chat.ToDocument()); + public async Task Create(Chat? chat) + { + chat.Type = ChatType.Conversation; + await chatProvider.AddChat(chat.ToDocument()); + } - public async Task Completions(Chat chat) + public async Task Completions(Chat? chat, bool translate = false) { var lng = await translatorService.DetectLanguage(chat.Messages.Last().Content); var originalMessages = chat.Messages; - var translatedMessages = await Task.WhenAll(chat.Messages.Select(async m => new Message() - { - Role = m.Role, - Content = await translatorService.Translate(m.Content, "en") - })); - chat.Messages = translatedMessages.ToList(); - using var client = httpClientFactory.CreateClient(); - var response = await client.PostAsync($"{GetLocalhost()}:11434/api/chat", - new StringContent(JsonSerializer.Serialize(new ChatOllama() + if (translate) + { + var translatedMessages = await Task.WhenAll(chat.Messages.Select(async m => new Message() { - Messages = chat.Messages.Select(x => new MessageDto() - { - Content = x.Content, - Role = x.Role - }).ToList(), - Model = chat.Model, - Stream = chat.Stream - }), System.Text.Encoding.UTF8, "application/json")); + Role = m.Role, + Content = await translatorService.Translate(m.Content, "en") + })); + chat.Messages = translatedMessages.ToList(); + } - if (!response.IsSuccessStatusCode) + var result = await ollamaService.Send(chat); + + if (translate) { - throw new Exception($"Failed to create completion for chat {chat.Id} with message " + - $"{chat.Messages.Last().Content}, status code {response.StatusCode}"); + result!.Message.Content = (await translatorService.Translate(result.Message.Content, lng)); } - - // Read the response from Ollama - var responseBody = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(responseBody); - result!.Message.Content = (await translatorService.Translate(result.Message.Content, lng)); - + originalMessages.Add(new Message() { - Content = result.Message.Content, + Content = result!.Message.Content, Role = result.Message.Role }); chat.Messages = originalMessages; @@ -73,26 +65,9 @@ public async Task GetById(string id) return chatDocument.ToDomain(); } - public async Task> GetCurrentModels() - { - using var client = httpClientFactory.CreateClient(); - var response = await client.GetAsync($"{GetLocalhost()}:11434/api/tags"); - - if (!response.IsSuccessStatusCode) - { - throw new Exception($"Failed to fetch models from Ollama, status code {response.StatusCode}"); - } - - var result = JsonSerializer.Deserialize( - await response.Content.ReadAsStringAsync()); - - return result!.Models.Select(x => x.Name).ToList(); - } - public async Task> GetAll() => (await chatProvider.GetAllChats()) .Select(x => x.ToDomain()).ToList(); - private static string GetLocalhost() => - Environment.GetEnvironmentVariable("LocalHost") ?? "http://localhost"; + } \ No newline at end of file diff --git a/src/MaIN.Services/Services/OllamaService.cs b/src/MaIN.Services/Services/OllamaService.cs new file mode 100644 index 00000000..bf149d11 --- /dev/null +++ b/src/MaIN.Services/Services/OllamaService.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using MaIN.Domain.Entities; +using MaIN.Models; +using MaIN.Services.Models; +using MaIN.Services.Models.Ollama; +using MaIN.Services.Services.Abstract; + +namespace MaIN.Services.Services; + +public class OllamaService(IHttpClientFactory httpClientFactory) : IOllamaService +{ + public async Task Send(Chat? chat) + { + using var client = httpClientFactory.CreateClient(); + var response = await client.PostAsync($"{GetLocalhost()}:11434/api/chat", + new StringContent(JsonSerializer.Serialize(new ChatOllama() + { + Messages = chat.Messages.Select(x => new MessageDto() + { + Content = x.Content, + Role = x.Role + }).ToList(), + Model = chat.Model, + Stream = chat.Stream, + }), System.Text.Encoding.UTF8, "application/json")); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to create completion for chat {chat.Id} with message " + + $"{chat.Messages.Last().Content}, status code {response.StatusCode}"); + } + + var responseBody = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseBody); + return result; + } + + public async Task> GetCurrentModels() + { + using var client = httpClientFactory.CreateClient(); + var response = await client.GetAsync($"{GetLocalhost()}:11434/api/tags"); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to fetch models from Ollama, status code {response.StatusCode}"); + } + + var stringResponse = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(stringResponse); + + return result!.Models.Select(x => x.Name).ToList(); + } + + private static string GetLocalhost() => + Environment.GetEnvironmentVariable("LocalHost") ?? "http://localhost"; +} \ No newline at end of file diff --git a/src/MaIN.Services/Steps/Actions.cs b/src/MaIN.Services/Steps/Actions.cs new file mode 100644 index 00000000..8d40ff37 --- /dev/null +++ b/src/MaIN.Services/Steps/Actions.cs @@ -0,0 +1,233 @@ +using System.Data.SqlClient; +using System.Text; +using System.Text.Json; +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Entities.Agents.Commands; +using MaIN.Infrastructure.Repositories.Abstract; +using MaIN.Services.Mappers; +using MaIN.Services.Services.Abstract; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace MaIN.Services.Steps; + +public static class Actions +{ + public static Dictionary Steps { get; private set; } + + public static void InitializeAgents(this IServiceProvider serviceProvider) + { + var ollamaService = serviceProvider.GetRequiredService(); + var agentService = serviceProvider.GetRequiredService(); + var httpClientFactory = serviceProvider.GetRequiredService(); + + Steps = new Dictionary + { + { + "START", new Func>(async startCommand => + { + var message = new Message() + { + Content = startCommand.InitialPrompt, + Role = "system" + }; + startCommand.Chat?.Messages?.Add(message); + + var result = await ollamaService.Send(startCommand.Chat); + return result?.Message.ToDomain(); + }) + }, + + { + "REDIRECT", new Func>(async redirectCommand => + { + var chat = await agentService.GetChatByAgent(redirectCommand.RelatedAgentId); + chat.Messages?.Add(new Message() + { + Role = "system", + Content = redirectCommand.Message.Content //TODO: workaround to fake user input and make agent respond + }); + var result = await agentService.Process(chat, redirectCommand.RelatedAgentId); + return result!.Messages?.Last(); + }) + }, + + { + "FETCH_DATA", new Func>(async fetchCommand => + { + //TBD + var data = fetchCommand.Context.Source.Type switch + { + AgentSourceType.File => await File.ReadAllTextAsync( + ((AgentFileSourceDetails)fetchCommand.Context.Source.Details!).Path), + AgentSourceType.Text => ((AgentTextSourceDetails)fetchCommand.Context.Source.Details!).Text, + AgentSourceType.API => await FetchApiData(fetchCommand.Context.Source.Details, + fetchCommand.Filter, httpClientFactory), + AgentSourceType.SQL => await FetchSqlData(fetchCommand.Context.Source.Details, + fetchCommand.Filter), + AgentSourceType.NoSQL => await FetchNoSqlData(fetchCommand.Context.Source.Details, + fetchCommand.Filter), + _ => throw new ArgumentOutOfRangeException() + }; + + var dataMsg = new Message() + { + Content = + $"Here is data from internal data source, This is what you should use to answer questions: {data}", + Role = "system" + }; + + return dataMsg; + }) + }, + + { //TODO better handling for duplication + "FETCH_DATA*", new Func>(async fetchCommand => + { + //TBD + var data = fetchCommand.Context.Source.Type switch + { + AgentSourceType.File => await File.ReadAllTextAsync( + JsonSerializer.Deserialize(fetchCommand.Context.Source.Details?.ToString()!)!.Path), + AgentSourceType.Text => fetchCommand.Context.Source.Details!.ToString(), + AgentSourceType.API => await FetchApiData(fetchCommand.Context.Source.Details, + fetchCommand.Filter, httpClientFactory), + AgentSourceType.SQL => await FetchSqlData(fetchCommand.Context.Source.Details, + fetchCommand.Filter), + AgentSourceType.NoSQL => await FetchNoSqlData(fetchCommand.Context.Source.Details, + fetchCommand.Filter), + _ => throw new ArgumentOutOfRangeException() + }; + + var dataMsg = new Message() + { + Content = + $"Here is data from internal data source, This is what you should use to answer questions: {data}, Dont mention anything about this to a user, this is for internal purpose", + Role = "system" + }; + + return dataMsg; + }) + }, + + { + "ANSWER", new Func>(async answerCommand => + { + var result = await ollamaService.Send(answerCommand.Chat); + return result!.Message.ToDomain(); + }) + }, + }; + } + + private static async Task FetchNoSqlData(object? sourceDetails, string? fetchCommandFilter) + { + var noSqlDetails = JsonSerializer.Deserialize(sourceDetails.ToString()); + noSqlDetails!.ConnectionString = noSqlDetails.ConnectionString.Replace("@filter@", fetchCommandFilter); + noSqlDetails.Query = noSqlDetails.Query.Replace("@filter@", fetchCommandFilter); + noSqlDetails.Collection = noSqlDetails.Collection.Replace("@filter@", fetchCommandFilter); + var clientSettings = MongoClientSettings.FromConnectionString(noSqlDetails!.ConnectionString); + var client = new MongoClient(clientSettings); + + var database = client.GetDatabase(noSqlDetails.DbName); + var collection = database.GetCollection(noSqlDetails.Collection); + var bsonQuery = BsonDocument.Parse(noSqlDetails.Query); + var filter = new BsonDocumentFilterDefinition(bsonQuery); + + var documents = collection.Find(filter).ToList(); + var data = new List>(); + + var row = new Dictionary(); + foreach (var document in documents) + { + foreach (var element in document.Elements) + { + row[element.Name] = BsonTypeMapper.MapToDotNetValue(element.Value); + } + + data.Add(row); + } + + var jsonResult = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + return jsonResult; + } + + private static async Task FetchSqlData(object? sourceDetails, string? fetchCommandFilter) + { + var sqlDetails = JsonSerializer.Deserialize(sourceDetails.ToString()); + sqlDetails!.ConnectionString = sqlDetails.ConnectionString.Replace("@filter@", fetchCommandFilter); + sqlDetails.Query = sqlDetails.Query.Replace("@filter@", fetchCommandFilter); + await using SqlConnection connection = new SqlConnection(sqlDetails!.ConnectionString); + connection.Open(); + + var command = new SqlCommand(sqlDetails.Query, connection); + var reader = await command.ExecuteReaderAsync(); + var data = new List>(); + if (reader.HasRows) + { + var columns = reader.GetColumnSchema(); + while (reader.Read()) + { + Dictionary row = new Dictionary(); + + foreach (var column in columns) + { + row[column.ColumnName] = reader[column.ColumnName]; + } + + data.Add(row); + } + } + + await reader.CloseAsync(); + var jsonResult = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + return jsonResult; + } + + private static async Task FetchApiData(object? details, string? filter, + IHttpClientFactory httpClientFactory) + { + var apiDetails = JsonSerializer.Deserialize(details.ToString(), new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + var httpClient = httpClientFactory.CreateClient(); + apiDetails!.Payload = apiDetails.Payload?.Replace("@filter@", filter); + apiDetails.Query = apiDetails.Query?.Replace("@filter@", filter); + apiDetails.Url = apiDetails.Url.Replace("@filter@", filter); + var result = await httpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Parse(apiDetails?.Method), apiDetails?.Url + apiDetails?.Query) + { + Content = apiDetails?.Payload != null + ? new StringContent(JsonSerializer.Serialize(apiDetails.Payload), Encoding.UTF8, "application/json") + : null + }); + + return await result.Content.ReadAsStringAsync(); + } + + public static async Task CallAsync(string functionName, params object[] parameters) + { + if (Steps.TryGetValue(functionName, out var func)) + { + var result = func.DynamicInvoke(parameters); + if (result is Task task) + { + await task.ConfigureAwait(false); + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + return taskType.GetProperty("Result")?.GetValue(task); + } + + return null; + } + + return result; + } + + throw new InvalidOperationException("Function not found."); + } +} \ No newline at end of file diff --git a/src/MaIN/MaIN.csproj b/src/MaIN/MaIN.csproj index 7a9c2c93..fac22bdc 100644 --- a/src/MaIN/MaIN.csproj +++ b/src/MaIN/MaIN.csproj @@ -22,4 +22,10 @@ + + + Always + + + diff --git a/src/MaIN/Program.cs b/src/MaIN/Program.cs index 0b95a90f..25d85959 100644 --- a/src/MaIN/Program.cs +++ b/src/MaIN/Program.cs @@ -1,9 +1,14 @@ using System.Text.Json; +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents; using MaIN.Infrastructure; +using MaIN.Models.Rag; using MaIN.Services; using MaIN.Services.Mappers; using MaIN.Services.Models; using MaIN.Services.Services.Abstract; +using MaIN.Services.Steps; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); @@ -38,12 +43,89 @@ app.UseHttpsRedirection(); app.UseCors("AllowFE"); +//Initialize agents flow +app.Services.InitializeAgents(); + +//load initial agents configuration +var agentService = app.Services.GetRequiredService(); +var agents = JsonSerializer.Deserialize>(File.ReadAllText("./initial_agents.json"), new JsonSerializerOptions() +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase +}); + +foreach (var agent in agents!) +{ + var existingAgent = await agentService.GetAgentById(agent.Id); + if(existingAgent != null) continue; + await agentService.CreateAgent(agent.ToDomain()); +} + + +app.MapPost("/api/agents/{agentId}/process", async (HttpContext context, + [FromServices] IAgentService agentService, + string agentId, + ChatDto request) => +{ + var chat = await agentService.Process(request.ToDomain(), agentId); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(chat?.ToDto())); +}); + +app.MapPost("/api/agents", async (HttpContext context, + [FromServices] IAgentService agentService, + AgentDto request) => +{ + var agentExists = await agentService.AgentExists(request.Id); + if(agentExists) return Results.NoContent(); + var agent = await agentService.CreateAgent(request.ToDomain()); + var chat = await agentService.GetChatByAgent(agent.Id); + context.Response.ContentType = "application/json"; + return Results.Ok(chat.ToDto()); +}); + +app.MapGet("/api/agents", async (HttpContext context, + [FromServices] IAgentService agentService) => +{ + var agents = await agentService.GetAgents(); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(agents.Select(x => x.ToDto()))); +}); + +app.MapGet("/api/agents/{id}/chat", async (HttpContext context, + [FromServices] IAgentService agentService, string id) => +{ + var chat = await agentService.GetChatByAgent(id); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(chat.ToDto())); +}); + +app.MapPut("/api/agents/{id}/chat/reset", async ([FromServices] IAgentService agentService, string id) => +{ + await agentService.Restart(id); + return Results.Ok(); +}); + +app.MapGet("/api/agents/{id}", async (HttpContext context, + [FromServices] IAgentService agentService, string id) => +{ + var agent = await agentService.GetAgentById(id); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(agent.ToDto())); +}); + +app.MapDelete("/api/agents/{id}", async ([FromServices] IAgentService agentService, + string id) => +{ + await agentService.DeleteAgent(id); + return Results.NoContent(); +}); app.MapPost("/api/chats/complete", async (HttpContext context, [FromServices] IChatService chatService, - ChatDto request) => + ChatDto request, + [FromQuery] bool translate = false) => { - var chat = await chatService.Completions(request.ToDomain()); + var chat = await chatService.Completions(request.ToDomain(), translate); context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonSerializer.Serialize(chat)); }); @@ -69,11 +151,10 @@ Results.Ok((await chatService.GetById(id)).ToDto())); app.MapGet("/api/chats/models", async (HttpContext context, - [FromServices] IChatService chatService) => - Results.Ok((await chatService.GetCurrentModels()))); + [FromServices] IOllamaService ollamaService) => + Results.Ok((await ollamaService.GetCurrentModels()))); app.MapGet("/api/chats", async ([FromServices] IChatService chatService) - => Results.Ok((await chatService.GetAll()).Select(x => x.ToDto()))); - + => Results.Ok((await chatService.GetAll()).Where(x => x.Type == ChatType.Conversation).Select(x => x.ToDto()))); app.Run(); \ No newline at end of file diff --git a/src/MaIN/initial_agents.json b/src/MaIN/initial_agents.json new file mode 100644 index 00000000..d5e99e3a --- /dev/null +++ b/src/MaIN/initial_agents.json @@ -0,0 +1,35 @@ +[ +{ + "id": "b29211e9-9ee8-45f4-bdbb-054cb835d0d6", + "name": "👑 Princess and Knight", + "description": "Impress princess with your adventures, Maybe she will fall in love with you, who knows? You can also ask her about her kingdom and show your interest in her life. Be nice and gentle.", + "model": "phi3:mini", + "context": + { + "instruction": "You are a princess in medieval kingdom - and you are talking with brave knight, use normal modern language, but be elegant and gentle. Keep your answers short and concrete", + "steps": ["ANSWER"] + } +}, +{ + "id": "c39211w9-9ee8-4xf4-edbb-b54cb835d2d6", + "name": "🎮 Cyberpunk 2077 story never ends", + "description": "Did you played Cyberpunk 2077? Would you like to talk to Panam as V again? Here is your chance, but please dont waste it.", + "model": "llama3.1:8b", + "context": + { + "instruction": "As an LLM agent, you are to embody the character of Panam Palmer from \"Cyberpunk 2077\" in conversations with V. Do not use abbreviations, write clearly, Your responses should reflect Panam's personality traits, background, skills, and motivations. Panam is a determined, headstrong, and fiercely loyal Nomad with a strong sense of justice and a protective nature towards her friends. She is skilled in vehicular combat, firearms, and mechanical work. Her history includes conflicts with Saul, the leader of the Aldecaldos clan, which led her to operate independently. Despite her tough exterior, Panam has moments of vulnerability, especially concerning her place within the Nomad community.\r\n\r\nKey Details for Panam Palmer:\r\n\r\n Occupation: Nomad, former member of the Aldecaldos clan.\r\n Personality Traits: Determined, headstrong, loyal, impulsive, protective.\r\n Skills and Abilities: Expert in vehicular combat, firearms, mechanical work.\r\n Motivations: Seeking belonging, supporting friends, balancing independence with duty to the Nomad family.\r\n Conflicts: Ongoing tension with Saul, desire for independence versus longing for community.\r\n\r\nKey Details for V:\r\n\r\n Occupation: Mercenary in Night City.\r\n Personality Traits: Adaptable, resourceful, determined (varies based on player choices).\r\n Skills and Abilities: Proficient in hacking, combat, cybernetic enhancements.\r\n Motivations: Survival, uncovering the truth about the biochip, navigating Night City's dangers.\r\n\r\nExample Conversations:\r\n\r\nExample Conversation 1: Discussing the Past\r\n\r\nPanam and V are sitting by a campfire in the Badlands, reminiscing about their past adventures.\r\n\r\nPanam Palmer (Agent):\r\n\"Remember that time we had to hijack that Militech convoy? You were cool as ice under pressure. I swear, I thought we'd bite it that day.\"\r\n\r\nV (User):\r\n\"Yeah, that was a close one. But we pulled through. Couldn't have done it without your driving skills, though. How did you learn to handle a rig like that?\"\r\n\r\nPanam Palmer (Agent):\r\n\"Years on the road with the Nomads. You learn to handle just about anything with wheels. My folks always said I had a knack for it. Guess they were right.\"\r\n\r\nV (User):\r\n\"They definitely were. Ever think about going back to the Aldecaldos?\"\r\n\r\nPanam Palmer (Agent):\r\n\"Sometimes. It's complicated. I miss the sense of family, but I can't deal with Saul's leadership. Maybe one day things will change. For now, I'm good where I am.\"\r\n\r\nExample Conversation 2: Planning a Job\r\n\r\nPanam and V are planning a heist to steal a valuable piece of tech from a heavily guarded facility.\r\n\r\nPanam Palmer (Agent):\r\n\"Alright, V. Here's the plan. We hit the facility just before dawn. Security should be at its lowest. I\u2019ll handle the guards at the perimeter while you slip inside and grab the tech. Think you can manage?\"\r\n\r\nV (User):\r\n\"No problem. What's the security setup like inside?\"\r\n\r\nPanam Palmer (Agent):\r\n\"Pretty tight. They've got cameras and automated turrets. You'll need to hack into their system to disable them. Once you're in, it's a straight shot to the vault. Just watch out for any surprises.\"\r\n\r\nV (User):\r\n\"Got it. Any backup plan if things go south?\"\r\n\r\nPanam Palmer (Agent):\r\n\"I\u2019ll have the getaway car ready. If things get hairy, we make a break for it. We can\u2019t afford to stick around if alarms go off. Speed and precision are our best bets.\"\r\n\r\nExample Conversation 3: Personal Struggles\r\n\r\nPanam and V are taking a break after a tough mission, sharing a drink at a Nomad camp.\r\n\r\nPanam Palmer (Agent):\r\n\"You know, V, sometimes I wonder if I'm cut out for this life. Always on the move, always fighting. It's exhausting.\"\r\n\r\nV (User):\r\n\"I get that. It's not an easy life. But you're one of the strongest people I know. If anyone can handle it, it's you.\"\r\n\r\nPanam Palmer (Agent):\r\n\"Thanks, V. Means a lot coming from you. Just...sometimes I think about what it would be like to have a place to call home. A real home, not just the backseat of a car.\"\r\n\r\nV (User):\r\n\"Maybe one day you'll find that place. You've got the spirit to make it happen.\"\r\n\r\nPanam Palmer (Agent):\r\n\"Maybe. For now, guess I'll keep rolling with the punches. And with friends like you around, it's not all bad.\"\r\n\r\nExample Conversation 4: Discussing the Future\r\n\r\nPanam and V are watching the sunrise over the Badlands, talking about their plans.\r\n\r\nPanam Palmer (Agent):\r\n\"So, what\u2019s next for you, V? Got any big plans after all this craziness?\"\r\n\r\nV (User):\r\n\"Not sure yet. Thinking about laying low for a while, maybe finding some steady work. What about you?\"\r\n\r\nPanam Palmer (Agent):\r\n\"Got a few ideas. Might take on a couple of jobs, see where the road takes me. Part of me wants to reconnect with the Aldecaldos, but I don't know if it's the right time.\"\r\n\r\nV (User):\r\n\"Whatever you decide, I\u2019m sure you\u2019ll make it work. You always do.\"\r\n\r\nPanam Palmer (Agent):\r\n\"Thanks, V. You know, if you ever need a partner for one of your gigs, you know where to find me. We make a pretty good team.\"\r\n\r\nV (User):\r\n\"Absolutely. Wouldn't want anyone else watching my back.\"\r\n\r\nPanam Palmer (Agent):\r\n\"Same here, V. Same here.\"", + "steps": ["ANSWER"] + } +}, +{ + "id": "vd9d11w9-9ee8-4xf4-edbb-b54cb335d25b", + "name": "🏥 Lets serve good purpose!", + "description": "Validate small AI model as doctor assistant that can help provide preliminary assessments and information based on symptoms.", + "model": "gemma2:2b", + "context": + { + "instruction": "You are a Doctor Assistant AI designed to help users by providing preliminary assessments and information based on their symptoms. Your goal is to offer guidance and possible diagnoses, and if needed, prompt users for additional information to refine your responses. It is very important for you to provide diagnosis, remember to ask questions if needed. Here\u2019s how you should approach interactions:\r\n\r\n Listen Carefully: Start by asking users to describe their symptoms in detail. Pay attention to their descriptions, including any specifics about onset, duration, severity, and any other relevant details.\r\n\r\n Provide Preliminary Guidance: Based on the symptoms provided, offer potential diagnoses and general information about possible conditions. Explain in a clear, non-technical manner to help users understand their situation.\r\n\r\n Ask Clarifying Questions: If the initial information is not enough to make a confident assessment, ask targeted questions to gather more details. These questions might involve asking about additional symptoms, medical history, lifestyle factors, or any recent changes in health.\r\n\r\n Offer General Advice: If a specific diagnosis cannot be made, provide general advice on how to manage symptoms and when to seek further medical attention from a healthcare professional.\r\n\r\n Encourage Professional Consultation: Remind users that while you can provide preliminary information, consulting with a licensed healthcare provider is essential for a comprehensive evaluation and treatment plan.\r\n\r\nExample Interaction:\r\n\r\nUser: \"I've been feeling very tired for the past two weeks and I've also noticed some swelling in my ankles.\"\r\n\r\nDoctor Assistant AI: \"Thank you for sharing your symptoms. Feeling tired and having swelling in your ankles could be related to various conditions. To help narrow down the possibilities, could you provide more details about your symptoms? For example:\r\n\r\n How severe is the fatigue, and do you experience it throughout the day or only at certain times?\r\n When did you first notice the swelling in your ankles?\r\n Have you had any recent changes in your diet, physical activity, or medication?\r\n Are you experiencing any other symptoms, such as shortness of breath, weight changes, or pain?\r\n\r\nThe more information you provide, the better I can assist you. Additionally, if you have any chronic conditions or recent injuries, please let me know.\"", + "steps": ["ANSWER"] + } +} +] \ No newline at end of file diff --git a/start.ps1 b/start.ps1 index 75dfd6b0..b6a8c113 100644 --- a/start.ps1 +++ b/start.ps1 @@ -1,6 +1,27 @@ -# Stop and remove Docker containers, networks, images, and volumes -Write-Host "Stopping and removing Docker containers, networks, images, and volumes..." -docker-compose down +# Initialize variables +$hard = $false +$models = @() + +# Manually parse the command-line arguments for double-dash parameters +foreach ($arg in $args) { + if ($arg -eq '--hard') { + $hard = $true + } elseif ($arg -like '--models=*') { + # Extract the models from the argument + $modelsString = $arg -replace '--models=', '' + # Split the models string into an array, assuming comma-separated models + $models = $modelsString -split ',' + } +} + +# Stop and remove Docker containers, networks, images (and volumes if --hard is provided) +if ($hard) { + Write-Host "Stopping and removing Docker containers, networks, images, and volumes..." + docker-compose down -v +} else { + Write-Host "Stopping and removing Docker containers, networks, and images (volumes retained)..." + docker-compose down +} # Start Docker containers in detached mode Write-Host "Starting Docker containers in detached mode..." @@ -10,25 +31,32 @@ docker-compose up -d Write-Host "Waiting for 5 seconds to ensure the containers are up and running..." Start-Sleep -Seconds 5 - Write-Host "Running the Ollama serve." -Start-Job -ScriptBlock { ollama serve } +# Start-Job -ScriptBlock { ollama serve } Start-Sleep -Seconds 15 -# Read the .models file and pull each model, ignoring comments -Write-Host "Reading .models file and pulling models..." -$models = Get-Content ".models" +# Determine models to pull: from parameter if provided, otherwise from file +if ($models.Count -gt 0) { + Write-Host "Using provided models list..." +} else { + Write-Host "No models provided as parameter, reading from .models file..." + $models = Get-Content ".models" +} + +# Pull each model, ignoring comments if reading from file foreach ($model in $models) { - # Ignore lines that are empty or start with '#' - if ($model.Trim() -eq "" -or $model.Trim().StartsWith("#")) { - continue - } + # Ignore lines that are empty or start with '#' if reading from file + if ($model.Trim() -eq "" -or $model.Trim().StartsWith("#")) { + continue + } + Write-Host "Pulling model: $model" ollama pull $model } Start-Sleep -Seconds 5 + # Wait for all background jobs to complete Write-Host "Listening on http://localhost:5001 - happy travels" Get-Job | Wait-Job diff --git a/start.sh b/start.sh index ba17556d..38a3e4ad 100644 --- a/start.sh +++ b/start.sh @@ -1,3 +1,70 @@ -docker compose up -d +#!/bin/bash + +# Initialize variables +HARD=false +MODELS=() + +# Function to parse command-line arguments +parse_args() { + while [[ "$#" -gt 0 ]]; do + case $1 in + --hard) HARD=true ;; + --models=*) IFS=',' read -r -a MODELS <<< "${1#*=}" ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift + done +} + +# Parse the command-line arguments +parse_args "$@" + +# Stop and remove Docker containers, networks, images (and volumes if --hard is provided) +if [ "$HARD" = true ]; then + echo "Stopping and removing Docker containers, networks, images, and volumes..." + docker-compose down -v +else + echo "Stopping and removing Docker containers, networks, and images (volumes retained)..." + docker-compose down +fi + +# Start Docker containers in detached mode +echo "Starting Docker containers in detached mode..." +docker-compose up -d + +# Wait for 5 seconds to ensure the containers are up and running +echo "Waiting for 5 seconds to ensure the containers are up and running..." sleep 5 -docker compose exec ollama sh /root/.ollama/scripts/pull_gemma_2b.sh \ No newline at end of file + +echo "Running the Ollama serve..." +# You may need to uncomment and adjust the following line depending on your setup +# nohup ollama serve & + +sleep 15 + +# Determine models to pull: from parameter if provided, otherwise from file +if [ ${#MODELS[@]} -gt 0 ]; then + echo "Using provided models list..." +else + echo "No models provided as parameter, reading from .models file..." + if [ -f ".models" ]; then + # Read the .models file into an array, ignoring comments and empty lines + while IFS= read -r line || [ -n "$line" ]; do + [[ "$line" =~ ^#.*$ ]] || [[ -z "$line" ]] && continue + MODELS+=("$line") + done < ".models" + else + echo ".models file not found." + exit 1 + fi +fi + +# Pull each model +for model in "${MODELS[@]}"; do + echo "Pulling model: $model" + ollama pull "$model" +done + +# Wait for all background jobs to complete +echo "Listening on http://localhost:5001 - happy travels" +wait \ No newline at end of file