Henry

Henry is a lightweight, extensible client-server framework for building autonomous AI agents powered by Large Language Models (LLMs). It seamlessly bridges local client-side tool execution with secure, centralized LLM processing and persistent memory.

Architecture Overview

The solution is divided into three core projects:

  1. Henry.Client (net10.0): A "thin" SDK that manages the connection to the server. It handles conversational state deltas, automatically extracts tool definitions from your C# code via Reflection, and autonomously executes tool calls requested by the LLM.
  2. Henry.Server (net10.0): An ASP.NET Core Minimal API acting as the single source of truth. It manages SQLite-based conversation persistence, securely proxies requests to the LLM (e.g., Qwen via OpenAI SDK), and handles context pruning.
  3. Henry.CLI (net10.0): A reference console application demonstrating how to consume Henry.Client to build an interactive terminal agent.

Key Features

  • Thin Client Architecture: The client only transmits new message deltas over the network. The server maintains the master conversation history in a database, making the architecture highly scalable and network-efficient.
  • Autonomous Tool Execution: Simply annotate a method with [HenryTool], and Henry.Client will automatically generate a JSON schema, pass it to the LLM, intercept the LLM's tool execution request, run your local C# method, and feed the result back to the LLM.
  • Server & Client Tools: Tools can be hosted on the client SDK or natively on the server. The server can execute tasks, interact with databases, and save documents directly on the backend, seamlessly injecting the results back into the context.
  • Persistent Conversations: Pick up right where you left off. The server stores all chat history—including the LLM's internal tool invocations (scratchpad memory)—in a SQLite database.
  • Clean UI: Internal tool calls and system prompts are flagged with IsInternal = true, ensuring the LLM retains its thought process in the database, while the presentation layer stays clean and human-readable.

Getting Started

1. Server Configuration

Create a .env file in the Henry.Server directory:

HENRY_SERVER_DB_PROVIDER=sqlite
HENRY_SERVER_CONNECTION_STRING=Data Source=henry.db
HENRY_LLM_ENDPOINT_URL=http://localhost:11434/v1
HENRY_LLM_API_KEY=dummy-key
HENRY_LLM_MODEL_NAME=Qwen3.6-35B-A3B-UD-Q4_K_M

# Optional Server Configuration
HENRY_SERVER_LOG_LEVEL=Warning # Options: Trace, Debug, Information, Warning, Error, Critical
HENRY_FILE_PATH=~/.henry/files # Path where the server stores created files and documents

Provider-Agnostic Storage

Henry Server supports multiple Entity Framework Core database providers out of the box. You can seamlessly switch providers just by changing the .env configuration. The database schema will be automatically created and migrated on startup.

SQLite (Default):

HENRY_SERVER_DB_PROVIDER=sqlite
HENRY_SERVER_CONNECTION_STRING=Data Source=henry.db

PostgreSQL:

HENRY_SERVER_DB_PROVIDER=postgres
HENRY_SERVER_CONNECTION_STRING=Host=my_host;Database=my_db;Username=my_user;Password=my_password

MySQL / MariaDB:

HENRY_SERVER_DB_PROVIDER=mysql
HENRY_SERVER_CONNECTION_STRING=Server=my_server;Port=3306;Database=my_db;Uid=my_user;Pwd=my_password;SslMode=Required;

SQL Server (MSSQL):

HENRY_SERVER_DB_PROVIDER=mssql
HENRY_SERVER_CONNECTION_STRING=Server=my_server;Database=my_db;User Id=my_user;Password=my_password;TrustServerCertificate=True;

Start the server: dotnet run --project Source/Henry.Server

Note: Upon the first launch, the server automatically initializes the database and creates a default administrator account. You can log into the Admin Dashboard using the username admin and password admin.

2. Usage Example (Henry.Client)

using Henry.Client;

// 1. Initialize the client
var httpClient = new HttpClient { BaseAddress = new Uri("http://localhost:5025") };
var client = new HenryClient(httpClient);

// 2. Register tools automatically via Reflection
client.RegisterToolsFromNamespace("MyApp.Tools");

// 3. Start a new conversation
client.StartConversation();

// 4. Chat loop
client.AddUserMessage("What time is it?");
var response = await client.GenerateResponseAsync();

Console.WriteLine($"Henry: {response.Content}");

// 5. Track created or modified files
if (client.Files.Any())
{
    foreach (var file in client.Files)
    {
        Console.WriteLine($"New file created: {file.FileName} ({file.FileGuid})");
    }
}

3. Uploading User Files

Consumers can supply contextual documents to the LLM. The HenryClient uploads files seamlessly, injecting the file contents straight into the conversation memory:

// Upload a document. If ConversationId is null, a new conversation is automatically started.
var uploadedGuid = await client.UploadFileAsync("/path/to/Market_Research.pdf");

// The LLM can now read the shadow-parsed markdown context of your file!
client.AddUserMessage("Please review the file I just uploaded.");
var response = await client.GenerateResponseAsync();

4. Creating Tools

To expose a local function to the LLM, define a static class and use the [HenryTool] attribute. Use [HenrySystemParameter] for parameters that should be auto-injected by the system instead of requested from the LLM (like Guid conversationId):

namespace MyApp.Tools;

public static class TimeTools 
{
    [HenryTool("get_current_time", "Returns the current date and local time.")]
    public static string GetCurrentTime([HenrySystemParameter] Guid conversationId)
    {
        return $"The current date and time for conversation {conversationId} is {DateTime.Now}";
    }
}

5. File Management & Web API

Henry Server ships with a built-in document engine. Server-side tools can generate PDFs, Word documents, and text files. You can retrieve files dynamically via the API:

  • Get File Content: GET /api/v1/files/{conversationId}/{fileGuid} (Downloads the latest version)
  • Get Specific Version: GET /api/v1/files/{conversationId}/{fileGuid}/version{version}
  • Get File Metadata: GET /api/v1/files/{conversationId}/{fileGuid}/metadata Returns JSON containing the file name, active versions, exact byte sizes, and download URLs.
  • Get Recent Files: GET /api/v1/files/recent/{conversationId}?since={timestamp}

Resuming Conversations

If you want to resume a previous conversation, simply call RecallConversationAsync with the conversation GUID:

var history = await client.RecallConversationAsync(guid);
// The client will automatically sync the latest tool schemas with the server!
Description
No description provided
Readme 1.6 MiB
Languages
HTML 48%
C# 43.1%
CSS 8.7%
Dockerfile 0.2%