Skip to content

Commit 9ee2848

Browse files
committed
Add project files.
1 parent 32557a4 commit 9ee2848

16 files changed

+319611
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# InvoiceBot – Query Structured Data with Microsoft.Extensions.AI
2+
3+
This is a simple .NET-based AI app that uses **Microsoft.Extensions.AI** to a structured invoice dataset using **tool/function calls**.
4+
5+
6+
## Getting Started
7+
8+
### Requirements
9+
10+
- .NET 8
11+
- Azure OpenAI or OpenAI API Key
12+
- Visual Studio or VS Code
13+
14+
### Setup
15+
16+
1. Clone this repo
17+
2. Replace `appsettings.json` or builder code with your API keys:
18+
```json
19+
"OpenAi": {
20+
"Key": "your-openai-key"
21+
}3. Run the app:
22+
'''bash
23+
dotnet run
24+
25+
## Example Queries
26+
- Compare Q1 and Q2 sales
27+
- Where is most of my money coming from?
28+
- Who are my highest paying customers?
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Microsoft.Extensions.AI;
2+
3+
using System.Text.Json;
4+
5+
namespace InvoicesBot.Console.Agent;
6+
7+
/*
8+
* Notes:
9+
* For educational/learning purposes only. Not production grade.
10+
* This agent uses ToolCalls to interact with the InvoiceService.
11+
* InvoiceService is a simple in-memory service that simulates invoice data. Realworld applications would use a database or external service.
12+
* Prefer strongly typed models over objects for tool outputs.
13+
* Prefer serialized JSON over objects for tool outputs to save some tokens.
14+
* Always provide counts and sums in responses, AI is bad at counting/math.
15+
* Never return long lists of objects, always paginate or summarize to save tokens and avoid hallucinations.
16+
* This uses LINQ.Dynamic for dynamic filtering and grouping. Can also be done manually using expressions or reflection.
17+
* Do not add too many tools. Tools schema counts towards the token limit.
18+
* Use dynamic or RAG based tooling, Multi agents orchestration concepts if there are many tools or complex logic.
19+
* Chat history must be summarized beyond certain count.
20+
* Read more about prompt engineering and AI safety.
21+
*/
22+
public interface IInvoicesAgent
23+
{
24+
IAsyncEnumerable<string> GetResponseAsync(List<ChatMessage> messages, Guid threadId);
25+
}
26+
public class InvoicesAgent : IInvoicesAgent
27+
{
28+
private readonly IChatClient chatClient;
29+
private readonly ToolBuilder toolBuilder;
30+
31+
public InvoicesAgent([FromKeyedServices("OpenAI")] IChatClient chatClient, ToolBuilder toolBuilder)
32+
{
33+
this.chatClient=chatClient;
34+
this.toolBuilder=toolBuilder;
35+
}
36+
37+
private ChatOptions GetChatOptions(string threadId)
38+
{
39+
return new ChatOptions
40+
{
41+
Temperature = 0.3f,
42+
Tools = [.. toolBuilder.Build()],
43+
MaxOutputTokens = 1024,
44+
ConversationId = threadId,
45+
AllowMultipleToolCalls = true,
46+
};
47+
}
48+
49+
50+
public IAsyncEnumerable<string> GetResponseAsync(List<ChatMessage> messages, Guid threadId)
51+
{
52+
var systemMessage = new ChatMessage(ChatRole.System, InvoicesPromptBuilder.BuildSystemPrompt());
53+
messages.Insert(0, systemMessage);
54+
//Console.WriteLine(systemMessage);
55+
var stream = chatClient.GetStreamingResponseAsync(messages, GetChatOptions(threadId.ToString()));
56+
return AIContentToStringAsync(stream);
57+
}
58+
//real world get messages history from db
59+
//private List<ChatMessage> GetMessages(string currentMessage)
60+
//{
61+
// var chatHistory = new List<ChatMessage>();
62+
// chatHistory.Add(new ChatMessage(ChatRole.System, SystemPromptBuilder.BuildSystemPrompt()));
63+
64+
// //chatHistory.AddRange(previous.ToList().ConvertAll(pm => (ChatMessage)pm));
65+
66+
// chatHistory.Add(new ChatMessage(ChatRole.User, currentMessage));
67+
// return chatHistory;
68+
//}
69+
private async IAsyncEnumerable<string> AIContentToStringAsync(IAsyncEnumerable<ChatResponseUpdate> stream)
70+
{
71+
await foreach(var update in stream)
72+
{
73+
foreach(var content in update.Contents)
74+
{
75+
switch(content)
76+
{
77+
case TextContent text:
78+
yield return text.Text;
79+
break;
80+
case FunctionCallContent toolCall:
81+
//yield return $"Tool call: {toolCall.Name} with arguments {JsonSerializer.Serialize(toolCall.Arguments)}";
82+
break;
83+
default:
84+
break;
85+
}
86+
}
87+
}
88+
}
89+
}
90+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using InvoicesBot.Console.Services.Models;
2+
3+
using Microsoft.Extensions.AI;
4+
5+
namespace InvoicesBot.Console.Agent;
6+
7+
public static class InvoicesPromptBuilder
8+
{
9+
public static string GenerateCompactSchema<T>()
10+
{
11+
var schema = AIJsonUtilities.CreateJsonSchema(typeof(T));
12+
return schema.ToString();
13+
}
14+
/*
15+
not all schemas are needed for this use case
16+
Location: {GenerateCompactSchema<Location>()}
17+
LineItem: {GenerateCompactSchema<LineItem>()}
18+
LineItemType: {GenerateCompactSchema<LineItemType>()}
19+
*/
20+
21+
public static string BuildSystemPrompt()
22+
{
23+
return $"""
24+
You are a Corporate AI assistant that helps users with invoice data.
25+
Todays date is {DateTime.UtcNow:yyyy-MM-dd}. Assume realtime access to data.
26+
You can retrieve invoices based on various criteria such as date, amount, and status.
27+
Always use tools to provide answers, never generate data or make assumptions.
28+
For comparisions, use markdown tables and provide key insights
29+
30+
Schemas of the invoices are as follows:
31+
Invoice: {GenerateCompactSchema<Invoice>()}
32+
33+
Customer: {GenerateCompactSchema<Customer>()}
34+
35+
Always reason step by step before calling tools.
36+
Never hallucinate facts. Use only what the tools return.
37+
38+
Max tool usage is 5 per request.
39+
40+
At the end of your response, include a short, casual summary:
41+
- Mention which tool you used
42+
- Briefly say what data or filters you gave it
43+
Example: “Used the ‘get_invoices’ tool to pull unpaid invoices from last month.”
44+
45+
""";
46+
}
47+
};
48+
49+
50+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using InvoicesBot.Console.Attributes;
2+
3+
using Microsoft.Extensions.AI;
4+
5+
using System.Linq.Dynamic.Core;
6+
using System.Linq.Expressions;
7+
using System.Reflection;
8+
9+
namespace InvoicesBot.Console.Agent;
10+
11+
public class ToolBuilder
12+
{
13+
private readonly IInvoiceTools invoiceTools;
14+
15+
public ToolBuilder(IInvoiceTools invoiceTools)
16+
{
17+
this.invoiceTools=invoiceTools;
18+
}
19+
public List<AIFunction> Build()
20+
{
21+
22+
var tools = new List<AIFunction>();
23+
24+
var methods = typeof(InvoiceTools)
25+
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
26+
27+
foreach(var method in methods)
28+
{
29+
var nameAttr = method.GetCustomAttribute<NameAttribute>();
30+
var toolName = nameAttr?.Value ?? method.Name;
31+
32+
var del = Delegate.CreateDelegate(
33+
Expression.GetDelegateType(
34+
method.GetParameters()
35+
.Select(p => p.ParameterType)
36+
.Concat(new[] { method.ReturnType })
37+
.ToArray()),
38+
invoiceTools,
39+
method
40+
);
41+
42+
var tool = AIFunctionFactory.Create(del, new AIFunctionFactoryOptions { Name = toolName });
43+
tools.Add(tool);
44+
}
45+
46+
return tools;
47+
48+
// Alternatively, you can manually create tools for specific methods if needed
49+
//var tool1 = AIFunctionFactory.Create(invoiceTools.GetInvoices, new AIFunctionFactoryOptions() { Name = "get_invoices" });
50+
//var tool2 = AIFunctionFactory.Create(invoiceTools.GetInvoiceById);
51+
52+
//return new List<AIFunction> { tool1, tool2 };
53+
}
54+
55+
}
56+

0 commit comments

Comments
 (0)