Skip to content

Commit

Permalink
[Ongoing] Adding Marketing sample (#52)
Browse files Browse the repository at this point in the history
* boilerplate init

* agents work

* agents

* adding frontend

* Now we can call the actor directly, and indireclty

* Orleans packages need to be there in order serialization works. no clue why

* horrible code, making the signlar hub static

* more horrible code, now the frontend can send messages to the backend and receibe the answer

* marketing works. a lot to fix still

* adding a legal assistant

* breaking agents

* adding a signalr client

* adding AlreadyExistentUser

* cleaning up

* renaming solution

* cleaning to prep for push to upstream

* cleaning in prep for upstream

* cleaning and forcing agents to always have a state

* removing legal-assistant for now

* sln should not bethere

* creating the class using new T

* replacing Activator by where T : new

* removing infra from marketing sample

* Add state initialization in AiAgent

* changing namespace name, and creating an agent to interact with signalr

* signalr agent works fine. It just loops forever when connecting. I need to diferentiate if it already happened

* init Readme.md

* Using Semantic Kernel to run Dall-E

* Graphic designer does not ened its own openai client anylonger
  • Loading branch information
crgarcia12 authored May 6, 2024
1 parent 615db4f commit cbf4252
Show file tree
Hide file tree
Showing 84 changed files with 14,389 additions and 19 deletions.
29 changes: 29 additions & 0 deletions samples/marketing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [In progress] Marketing Saple application

This is a demo application that showcase the different features of the AI Agent framework.
There are five agents in this application that control the different areas of the UI autonomously.

The agents are designed to be able to interact with each other and the user to achieve their goals.
To do that each agent has

![Agents](readme-media/agents.png)


## Requirements to run locally
### Frontend
The latest version of Node.js and npm

### Backend
Visual Studio or Visual Studio code and the latest version of dotnet

## How to run the application locally

Execute Run.ps1. IF you are missing the config file the script will create an empty one for you and ask you to fill it out.
```
.\run.ps1
```

## How to debug the application locally
To debug the backend, you can simply open the solution in Visual Studio, and press F5 to start debugging.
Remember to copy `appsettings.local.template.json` to `appsettings.json` and fill out the values.</p>
The frontend is a NodeJS React application. You can debug it using Visual Studio code.
10 changes: 10 additions & 0 deletions samples/marketing/azure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json

name: ai-dev-team
services:
gh-flow:
project: "src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj"
language: csharp
host: containerapp
docker:
context: ../../../
Binary file added samples/marketing/readme-media/agents.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions samples/marketing/run.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function New-MarketingBackendSettings {
param(

)
if(Test-Path src/backend/appsettings.json) {
Write-Host "appsettings.json already exists"
return
}

Copy-Item src/backend/appsettings.local.template.json src/backend/appsettings.json
Write-Host "appsettings.json created"

if((Get-Content .\src\backend\appsettings.local.template.json -Raw | Select-String "<mandatory>") -ne $null) {
Write-Error "Please update the appsettings.json file with the correct values" -ErrorAction Stop
}
}

$backendProc = Start-Process powershell -ArgumentList '-NoExit', '-Command cd src/backend/; dotnet run'
$frontendProc = Start-Process powershell -ArgumentList '-NoExit', '-Command cd src/frontend/; npm run dev'

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Marketing.Events;
using Marketing.Options;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;

namespace Marketing.Agents;

[ImplicitStreamSubscription(Consts.OrleansNamespace)]
public class CommunityManager : AiAgent<CommunityManagerState>
{
protected override string Namespace => Consts.OrleansNamespace;

private readonly ILogger<GraphicDesigner> _logger;

public CommunityManager([PersistentState("state", "messages")] IPersistentState<AgentState<CommunityManagerState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<GraphicDesigner> logger)
: base(state, memory, kernel)
{
_logger = logger;
}

public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(EventTypes.UserConnected):
// The user reconnected, let's send the last message if we have one
string lastMessage = _state.State.History.LastOrDefault()?.Message;
if (lastMessage == null)
{
return;
}

SendDesignedCreatedEvent(lastMessage, item.Data["UserId"]);
break;

case nameof(EventTypes.ArticleCreated):
//var lastCode = _state.State.History.Last().Message;

_logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}. UserMessage: {item.Message}");

var context = new KernelArguments { ["input"] = AppendChatHistory(item.Message) };
string socialMediaPost = await CallFunction(CommunityManagerPrompts.WritePost, context);
_state.State.Data.WrittenSocialMediaPost = socialMediaPost;
SendDesignedCreatedEvent(socialMediaPost, item.Data["UserId"]);
break;

default:
break;
}
}

private async Task SendDesignedCreatedEvent(string socialMediaPost, string userId)
{
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(EventTypes.SocialMediaPostCreated),
Data = new Dictionary<string, string> {
{ "UserId", userId },
},
Message = socialMediaPost
});
}

public Task<String> GetArticle()
{
return Task.FromResult(_state.State.Data.WrittenSocialMediaPost);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Marketing.Agents;

public static class CommunityManagerPrompts
{
public static string WritePost = """
You are a Marketing community manager writer.
Write a tweet to promote what it is described bellow.
The tweet cannot be longer than 280 characters
Input: {{$input}}
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Marketing.Agents;

[GenerateSerializer]
public class CommunityManagerState
{
[Id(0)]
public string WrittenSocialMediaPost { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Marketing.Agents;

[GenerateSerializer]
public class GraphicDesignerState
{
[Id(0)]
public string imageUrl { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Marketing.Events;
using Marketing.Options;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.TextToImage;
using Orleans.Runtime;

namespace Marketing.Agents;

[ImplicitStreamSubscription(Consts.OrleansNamespace)]
public class GraphicDesigner : AiAgent<GraphicDesignerState>
{
protected override string Namespace => Consts.OrleansNamespace;

private readonly ILogger<GraphicDesigner> _logger;
private readonly IConfiguration _configuration;

public GraphicDesigner([PersistentState("state", "messages")] IPersistentState<AgentState<GraphicDesignerState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<GraphicDesigner> logger, IConfiguration configuration)
: base(state, memory, kernel)
{
_logger = logger;
_configuration = configuration;
}

public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(EventTypes.UserConnected):
// The user reconnected, let's send the last message if we have one
string lastMessage = _state.State.History.LastOrDefault()?.Message;
if (lastMessage == null)
{
return;
}

SendDesignedCreatedEvent(lastMessage, item.Data["UserId"]);

break;
case nameof(EventTypes.ArticleCreated):
_logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}. UserMessage: {item.Message}");

var dallEService = _kernel.GetRequiredService<ITextToImageService>();
var imageUri = await dallEService.GenerateImageAsync(item.Message, 1024, 1024);

_state.State.Data.imageUrl = imageUri;

SendDesignedCreatedEvent(imageUri, item.Data["UserId"]);

break;

default:
break;
}
}

private async Task SendDesignedCreatedEvent(string AbsoluteImageUri, string userId)
{
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(EventTypes.GraphicDesignCreated),
Data = new Dictionary<string, string> {
{ "UserId", userId },
},
Message = AbsoluteImageUri
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

namespace Marketing.Agents;
public static class GraphicDesignerPrompts
{
public static string GenerateImage = """
You are a Marketing community manager graphic designer.
Bellow is a campaing that you need to create a image for.
Create an image of maximum 500x500 pixels that could be use in social medias as a marketing iamge.
Input: {{$input}}
""";
}
48 changes: 48 additions & 0 deletions samples/marketing/src/backend/Agents/SignalR/SignalR.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Marketing.Events;
using Marketing.Options;
using Marketing.SignalRHub;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using System;
using System.Security.Policy;

namespace Marketing.Agents;

[ImplicitStreamSubscription(Consts.OrleansNamespace)]
public class SignalR : Agent
{
protected override string Namespace => Consts.OrleansNamespace;

private readonly ILogger<SignalR> _logger;
private readonly ISignalRService _signalRClient;

public SignalR(ILogger<SignalR> logger, ISignalRService signalRClient)
{
_logger = logger;
_signalRClient = signalRClient;
}

public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(EventTypes.ArticleCreated):
var writenArticle = item.Message;
await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], writenArticle, AgentTypes.Chat);
break;

case nameof(EventTypes.GraphicDesignCreated):
var imageUrl = item.Message;
await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], imageUrl, AgentTypes.GraphicDesigner);
break;

case nameof(EventTypes.SocialMediaPostCreated):
var post = item.Message;
await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], post, AgentTypes.CommunityManager);
break;

default:
break;
}
}
}
5 changes: 5 additions & 0 deletions samples/marketing/src/backend/Agents/Writer/IWritter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Marketing.Agents;
public interface IWriter : IGrainWithStringKey
{
Task<String> GetArticle();
}
73 changes: 73 additions & 0 deletions samples/marketing/src/backend/Agents/Writer/Writer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Marketing.Events;
using Marketing.Options;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;

namespace Marketing.Agents;

[ImplicitStreamSubscription(Consts.OrleansNamespace)]
public class Writer : AiAgent<WriterState>, IWriter
{
protected override string Namespace => Consts.OrleansNamespace;

private readonly ILogger<GraphicDesigner> _logger;

public Writer([PersistentState("state", "messages")] IPersistentState<AgentState<WriterState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<GraphicDesigner> logger)
: base(state, memory, kernel)
{
_logger = logger;
}

public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(EventTypes.UserConnected):
// The user reconnected, let's send the last message if we have one
string lastMessage = _state.State.History.LastOrDefault()?.Message;
if (lastMessage == null)
{
return;
}

SendDesignedCreatedEvent(lastMessage, item.Data["UserId"]);

break;

case nameof(EventTypes.UserChatInput):

_logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.UserChatInput)}. UserMessage: {item.Message}");

var context = new KernelArguments { ["input"] = AppendChatHistory(item.Message) };
string newArticle = await CallFunction(WriterPrompts.Write, context);

SendDesignedCreatedEvent(newArticle, item.Data["UserId"]);

break;
default:
break;
}
}

private async Task SendDesignedCreatedEvent(string writtenArticle, string userId)
{
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(EventTypes.ArticleCreated),
Data = new Dictionary<string, string> {
{ "UserId", userId },
{ "UserMessage", writtenArticle },
},
Message = writtenArticle
});
}


public Task<String> GetArticle()
{
return Task.FromResult(_state.State.Data.WrittenArticle);
}
}
12 changes: 12 additions & 0 deletions samples/marketing/src/backend/Agents/Writer/WriterPrompts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

namespace Marketing.Agents;
public static class WriterPrompts
{
public static string Write = """
You are a Marketing writer.
Write up to three paragraphs for a campain to promote what it is described bellow.
Bellow are a series of inputs from the user that you can use to create the campain.
If the input talks about twitter or images, dismiss it and return the same as before.
Input: {{$input}}
""";
}
Loading

0 comments on commit cbf4252

Please sign in to comment.