Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ if(AGENT_CPP_BUILD_TESTS)
target_link_libraries(test_tool PRIVATE common llama)
target_compile_features(test_tool PRIVATE cxx_std_17)

add_executable(test_grammar tests/test_grammar.cpp)
target_include_directories(test_grammar PRIVATE
src
tests
${LLAMA_SOURCE_DIR}/common
${LLAMA_SOURCE_DIR}/ggml/include
${LLAMA_SOURCE_DIR}/include
${LLAMA_SOURCE_DIR}/vendor
)
target_link_libraries(test_grammar PRIVATE model common llama)
target_compile_features(test_grammar PRIVATE cxx_std_17)

add_executable(test_callbacks tests/test_callbacks.cpp)
target_include_directories(test_callbacks PRIVATE
src
Expand All @@ -145,6 +157,7 @@ if(AGENT_CPP_BUILD_TESTS)

add_test(NAME ToolTests COMMAND test_tool)
add_test(NAME CallbacksTests COMMAND test_callbacks)
add_test(NAME GrammarTests COMMAND test_grammar)

if(AGENT_CPP_BUILD_MCP)
add_executable(test_mcp_client tests/test_mcp_client.cpp)
Expand All @@ -162,7 +175,7 @@ if(AGENT_CPP_BUILD_TESTS)
# On Windows, DLLs are placed in the bin/ directory by llama.cpp
# We need to add this directory to PATH so tests can find the DLLs
if(WIN32)
set_tests_properties(ToolTests CallbacksTests PROPERTIES
set_tests_properties(ToolTests CallbacksTests GrammarTests PROPERTIES
ENVIRONMENT "PATH=${CMAKE_BINARY_DIR}/bin\;$ENV{PATH}"
)
endif()
Expand Down Expand Up @@ -223,6 +236,19 @@ if(AGENT_CPP_BUILD_EXAMPLES)
target_link_libraries(context-engineering-example PRIVATE agent model common llama)
target_compile_features(context-engineering-example PRIVATE cxx_std_17)

# Grammar example
add_executable(grammar-example examples/grammar/grammar.cpp)
target_include_directories(grammar-example PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/examples/shared
${LLAMA_SOURCE_DIR}/common
${LLAMA_SOURCE_DIR}/ggml/include
${LLAMA_SOURCE_DIR}/include
${LLAMA_SOURCE_DIR}/vendor
)
target_link_libraries(grammar-example PRIVATE agent model common llama)
target_compile_features(grammar-example PRIVATE cxx_std_17)

# MCP client example (requires MCP support)
if(AGENT_CPP_BUILD_MCP)
add_executable(mcp-example examples/mcp/mcp.cpp)
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Building blocks for **local** agents in C++.

- **[Context Engineering](./examples/context-engineering/README.md)** - Use callbacks to manipulate the context between iterations of the agent loop.

- **[Grammar](./examples/grammar/README.md)** - Constrain model output to a GBNF grammar so every response matches a fixed structure.

- **[Memory](./examples/memory/README.md)** - Use tools that allow an agent to store and retrieve relevant information across conversations.

- **[Multi-Agent](./examples/multi-agent/README.md)** - Build a multi-agent system with weight sharing where a main agent delegates to specialized sub-agents.
Expand Down Expand Up @@ -70,6 +72,18 @@ Handles:
- Text generation with configurable sampling (temperature, top_p, top_k, etc.)
- KV cache management for efficient prompt caching

### Grammar-constrained output

Model output can be constrained to a [GBNF grammar](https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md) to enforce structured formats (e.g. JSON, yes/no answers):

```cpp
ModelConfig config;
config.grammar = agent_cpp::load_grammar_file("path/to/grammar.gbnf");
config.grammar_root = "root"; // optional, defaults to "root"
```

See the [Grammar example](./examples/grammar/README.md) for a full working demo.

## Tools

Tools extend the agent's capabilities beyond text generation. Each tool defines:
Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This directory contains example applications demonstrating agent.cpp capabilities.

## Grammar

The [grammar](./grammar) example demonstrates constraining model output to a GBNF grammar, so every response is guaranteed to match a fixed structure. You can also point it at a custom grammar file and root rule.

## Shared Utilities

The [shared](./shared) directory contains reusable helper components used across multiple examples. These are **not part of the public API** but can be useful as reference implementations.
Expand Down
24 changes: 24 additions & 0 deletions examples/grammar/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.14)
project(grammar-example VERSION 0.1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. ${CMAKE_CURRENT_BINARY_DIR}/agent-cpp)

add_executable(grammar-example grammar.cpp)

target_include_directories(grammar-example PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
${CMAKE_CURRENT_SOURCE_DIR}/../shared
${LLAMA_SOURCE_DIR}/common
${LLAMA_SOURCE_DIR}/ggml/include
${LLAMA_SOURCE_DIR}/include
${LLAMA_SOURCE_DIR}/vendor
)

target_link_libraries(grammar-example PRIVATE agent-cpp::agent common llama)
target_compile_features(grammar-example PRIVATE cxx_std_17)

message(STATUS "Grammar example configured.")
78 changes: 78 additions & 0 deletions examples/grammar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Grammar Example

This example shows how to constrain model output with a [GBNF grammar](https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md). Instead of relying on prompt instructions alone, the grammar is enforced at the sampler level, so every response matches it.

The demo is a minimal sentiment classifier: whatever the user types, the model replies with a single well-formed JSON object shaped like `{"sentiment": "positive" | "negative" | "neutral"}`.

## Building Blocks

### Grammar

[`sentiment.gbnf`](./sentiment.gbnf) defines the grammar used in this example:

```gbnf
root ::= "{" ws "\"sentiment\":" ws sentiment ws "}"
sentiment ::= "\"positive\"" | "\"negative\"" | "\"neutral\""
ws ::= [ \t\n]*
```

Load it with `agent_cpp::load_grammar_file` and set it on `ModelConfig::grammar`:

```cpp
auto model_config = agent_cpp::ModelConfig{};
model_config.grammar = agent_cpp::load_grammar_file("sentiment.gbnf");
model_config.grammar_root = "root"; // optional, this is the default
```

No tools are used in this example. The grammar alone is enough to constrain every response.

## Building

> [!IMPORTANT]
> Check the [llama.cpp build documentation](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) to find
> CMake flags you might want to pass depending on your available hardware.

```bash
cd examples/grammar

git -C ../.. submodule update --init --recursive

cmake -B build
cmake --build build -j$(nproc)
```

### Using a custom llama.cpp

If you have llama.cpp already downloaded:

```bash
cmake -B build -DLLAMA_CPP_DIR=/path/to/your/llama.cpp
cmake --build build -j$(nproc)
```

## Usage

```bash
./build/grammar-example -m "path-to-model.gguf"

# Use a different grammar file
./build/grammar-example -m "path-to-model.gguf" -g "path-to-grammar.gbnf"

# Use a different grammar root rule
./build/grammar-example -m "path-to-model.gguf" -g "path-to-grammar.gbnf" -r "start"
```

## Example

```console
$ ./build/grammar-example -m ../../granite-4.0-micro-Q8_0.gguf
> This library is exactly what I needed, thank you!
{"sentiment": "positive"}

> The build failed three times before I figured out the flag I was missing.
{"sentiment": "negative"}

> The package arrived on Tuesday.
{"sentiment": "neutral"}
>
```
110 changes: 110 additions & 0 deletions examples/grammar/grammar.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "agent.h"
#include "chat_loop.h"
#include "error.h"
#include "model.h"
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <vector>

static constexpr const char* DEFAULT_GRAMMAR_PATH = "sentiment.gbnf";
static constexpr const char* DEFAULT_GRAMMAR_ROOT = "root";

static void
print_usage(int /*unused*/, char** argv)
{
printf("\nexample usage:\n");
printf("\n %s -m model.gguf\n", argv[0]);
printf("\n");
printf("options:\n");
printf(" -m <path> Path to the GGUF model file (required)\n");
printf(" -g <path> Path to a GBNF grammar file (default: %s)\n",
DEFAULT_GRAMMAR_PATH);
printf(" -r <name> Grammar root rule (default: %s)\n",
DEFAULT_GRAMMAR_ROOT);
printf("\n");
}

int
main(int argc, char** argv)
{
std::string model_path;
std::string grammar_path = DEFAULT_GRAMMAR_PATH;
std::string grammar_root = DEFAULT_GRAMMAR_ROOT;

for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-m") == 0) {
if (i + 1 < argc) {
model_path = argv[++i];
} else {
print_usage(argc, argv);
return 1;
}
} else if (strcmp(argv[i], "-g") == 0) {
if (i + 1 < argc) {
grammar_path = argv[++i];
} else {
print_usage(argc, argv);
return 1;
}
} else if (strcmp(argv[i], "-r") == 0) {
if (i + 1 < argc) {
grammar_root = argv[++i];
} else {
print_usage(argc, argv);
return 1;
}
} else {
print_usage(argc, argv);
return 1;
}
}

if (model_path.empty()) {
print_usage(argc, argv);
return 1;
}

printf("Loading grammar from '%s'...\n", grammar_path.c_str());
auto model_config = agent_cpp::ModelConfig{};
try {
model_config.grammar = agent_cpp::load_grammar_file(grammar_path);
} catch (const agent_cpp::ModelError& e) {
fprintf(stderr, "error: %s\n", e.what());
return 1;
}
model_config.grammar_root = grammar_root;
model_config.n_ctx = 4096;
model_config.temp = 0.0F;

printf("Loading model...\n");
std::shared_ptr<agent_cpp::Model> model;
try {
model = agent_cpp::Model::create(model_path, model_config);
} catch (const agent_cpp::ModelError& e) {
fprintf(stderr, "error: %s\n", e.what());
return 1;
}
printf("Model loaded successfully\n");

// No tools are needed: the grammar alone constrains the output.
std::vector<std::unique_ptr<agent_cpp::Tool>> tools;

const std::string instructions =
"You are a sentiment classifier. Given a message from the user, "
"respond with your assessment of its sentiment. Do not explain your "
"reasoning, just answer directly.";

agent_cpp::Agent agent(
std::move(model), std::move(tools), {}, instructions);

printf("\nGrammar Demo ready!\n");
printf(" Every response is constrained by sentiment.gbnf, so the "
"model can only ever reply with {\"sentiment\": \"positive\" | "
"\"negative\" | \"neutral\"}, regardless of what you type.\n");
printf(" Type an empty line to quit.\n\n");

run_chat_loop(agent);
return 0;
}
3 changes: 3 additions & 0 deletions examples/grammar/sentiment.gbnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
root ::= "{" ws "\"sentiment\":" ws sentiment ws "}"
sentiment ::= "\"positive\"" | "\"negative\"" | "\"neutral\""
ws ::= [ \t\n]*
Loading