solid_agent 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +321 -0
- data/Rakefile +32 -0
- data/docs/agent-md-spec.md +803 -0
- data/docs/parser-design.md +1369 -0
- data/docs/registry-api.md +882 -0
- data/examples/README.md +60 -0
- data/examples/manifests/changelog_writer.agent.md +81 -0
- data/examples/manifests/usage.rb +96 -0
- data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
- data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
- data/examples/memory_handoff/usage.rb +45 -0
- data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
- data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
- data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
- data/examples/persistent_conversation/usage.rb +51 -0
- data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
- data/examples/reasoning/usage.rb +52 -0
- data/examples/run_tracking/app/agents/report_agent.rb +30 -0
- data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
- data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
- data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
- data/examples/run_tracking/usage.rb +85 -0
- data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
- data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
- data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
- data/examples/tool_streaming/usage.rb +47 -0
- data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
- data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
- data/lib/generators/solid_agent/context/context_generator.rb +124 -0
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
- data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
- data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
- data/lib/generators/solid_agent/install/install_generator.rb +92 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
- data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
- data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
- data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
- data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
- data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
- data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
- data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
- data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
- data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
- data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
- data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
- data/lib/solid_agent/agent_manifest/errors.rb +26 -0
- data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
- data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
- data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
- data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
- data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
- data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
- data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
- data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
- data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
- data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
- data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
- data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
- data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
- data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
- data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
- data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
- data/lib/solid_agent/agent_manifest/resource.rb +103 -0
- data/lib/solid_agent/agent_manifest/tool.rb +160 -0
- data/lib/solid_agent/agent_manifest/validator.rb +368 -0
- data/lib/solid_agent/agent_manifest.rb +381 -0
- data/lib/solid_agent/engine.rb +16 -0
- data/lib/solid_agent/has_context.rb +670 -0
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -0
- data/lib/solid_agent/has_tools.rb +257 -0
- data/lib/solid_agent/model_naming.rb +42 -0
- data/lib/solid_agent/model_pricing.rb +93 -0
- data/lib/solid_agent/reasonable/reason.rb +205 -0
- data/lib/solid_agent/reasonable.rb +181 -0
- data/lib/solid_agent/records/agent.rb +520 -0
- data/lib/solid_agent/records/agent_run.rb +520 -0
- data/lib/solid_agent/records/agent_template.rb +142 -0
- data/lib/solid_agent/records/agent_version.rb +141 -0
- data/lib/solid_agent/records/ownable.rb +130 -0
- data/lib/solid_agent/records.rb +152 -0
- data/lib/solid_agent/run_fingerprint.rb +51 -0
- data/lib/solid_agent/streams_tool_updates.rb +178 -0
- data/lib/solid_agent/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +5 -0
- data/lib/solid_agent.rb +95 -0
- data/sig/solid_agent.rbs +4 -0
- metadata +174 -14
data/examples/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SolidAgent Examples
|
|
2
|
+
|
|
3
|
+
Runnable, copy-pasteable examples for every SolidAgent concern. Each
|
|
4
|
+
directory mirrors the layout of a Rails app, so the files can be dropped
|
|
5
|
+
into `app/` as-is and the paths tell you where they belong.
|
|
6
|
+
|
|
7
|
+
Every example assumes the persistence tables and models are installed:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add solid_agent
|
|
11
|
+
rails generate solid_agent:install
|
|
12
|
+
rails db:migrate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That generates `AgentContext`, `AgentMessage`, `AgentGeneration`,
|
|
16
|
+
`AgentMemory`, `AgentMemoryEntry` and `AgentRun` into `app/models/`, so they
|
|
17
|
+
are yours to edit — SolidAgent's concerns talk to them through a duck-typed
|
|
18
|
+
contract, not through hard-coded class names.
|
|
19
|
+
|
|
20
|
+
The narrative walkthrough of these examples lives at
|
|
21
|
+
[docs.activeagents.ai/solid_agent/examples](https://docs.activeagents.ai/solid_agent/examples).
|
|
22
|
+
|
|
23
|
+
| Example | Concerns | What it shows |
|
|
24
|
+
|---------|----------|---------------|
|
|
25
|
+
| [persistent_conversation](persistent_conversation) | `HasContext` | A support agent whose conversation survives the request, replayed from the database on every turn |
|
|
26
|
+
| [memory_handoff](memory_handoff) | `HasMemory` | Two agents sharing agent-curated notes about the same subject record |
|
|
27
|
+
| [tool_streaming](tool_streaming) | `HasTools`, `StreamsToolUpdates`, `ToolCache` | Declarative tool schemas, live "what is it doing" updates over ActionCable, cached tool results |
|
|
28
|
+
| [reasoning](reasoning) | `HasReasons`, `Reasonable` | Capturing extended-thinking output and persisting it on generation records |
|
|
29
|
+
| [run_tracking](run_tracking) | `AgentRun`, `RunFingerprint`, `ModelPricing` | Durable run records, an append-only progress stream a UI can poll, cohorts and cost |
|
|
30
|
+
| [manifests](manifests) | `AgentManifest` | Defining an agent in a portable `.agent.md` file and loading it as a class |
|
|
31
|
+
|
|
32
|
+
## Running the examples
|
|
33
|
+
|
|
34
|
+
The `usage.rb` file in each directory is the console script — the part you
|
|
35
|
+
would paste into `rails console` (or call from a controller/job) once the
|
|
36
|
+
agent files are in place. They are written to be read top to bottom rather
|
|
37
|
+
than executed blind: they hit a provider and write rows.
|
|
38
|
+
|
|
39
|
+
Prefer to try one without spending tokens? Point the agent at the mock
|
|
40
|
+
provider first:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class SupportAgent < ApplicationAgent
|
|
44
|
+
generate_with :mock, model: "mock-gpt-4o-mini"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Persistence, memory, runs and the tool cache all behave identically — only
|
|
49
|
+
the model response changes.
|
|
50
|
+
|
|
51
|
+
## Related documentation
|
|
52
|
+
|
|
53
|
+
- [SolidAgent overview](https://docs.activeagents.ai/solid_agent)
|
|
54
|
+
- [Conversation context](https://docs.activeagents.ai/solid_agent/context)
|
|
55
|
+
- [Long-term memory](https://docs.activeagents.ai/solid_agent/memory)
|
|
56
|
+
- [Tools, streaming and caching](https://docs.activeagents.ai/solid_agent/tools)
|
|
57
|
+
- [Reasoning](https://docs.activeagents.ai/solid_agent/reasoning)
|
|
58
|
+
- [Runs, cohorts and cost](https://docs.activeagents.ai/solid_agent/runs)
|
|
59
|
+
- [Agent manifests](https://docs.activeagents.ai/solid_agent/manifests)
|
|
60
|
+
- [`.agent.md` specification](../docs/agent-md-spec.md)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: changelog-writer
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Turns a range of merged pull requests into a release changelog
|
|
5
|
+
author: activeagents
|
|
6
|
+
license: MIT
|
|
7
|
+
tags:
|
|
8
|
+
- writing
|
|
9
|
+
- release
|
|
10
|
+
|
|
11
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
12
|
+
config:
|
|
13
|
+
temperature: 0.3
|
|
14
|
+
max_tokens: 2048
|
|
15
|
+
|
|
16
|
+
input:
|
|
17
|
+
schema:
|
|
18
|
+
repository: "string, The repository the release belongs to"
|
|
19
|
+
from?: "string, Git ref the release starts at"
|
|
20
|
+
to?: "string, Git ref the release ends at"
|
|
21
|
+
audience?: "string(users, operators, contributors), Who the changelog is written for"
|
|
22
|
+
|
|
23
|
+
output:
|
|
24
|
+
format: json
|
|
25
|
+
schema:
|
|
26
|
+
type: object
|
|
27
|
+
properties:
|
|
28
|
+
headline:
|
|
29
|
+
type: string
|
|
30
|
+
entries:
|
|
31
|
+
type: array
|
|
32
|
+
items:
|
|
33
|
+
type: object
|
|
34
|
+
properties:
|
|
35
|
+
title:
|
|
36
|
+
type: string
|
|
37
|
+
category:
|
|
38
|
+
type: string
|
|
39
|
+
breaking:
|
|
40
|
+
type: array
|
|
41
|
+
items:
|
|
42
|
+
type: string
|
|
43
|
+
|
|
44
|
+
tools:
|
|
45
|
+
- name: list_merged_pulls
|
|
46
|
+
description: List pull requests merged between two refs
|
|
47
|
+
inputSchema:
|
|
48
|
+
type: object
|
|
49
|
+
properties:
|
|
50
|
+
repository:
|
|
51
|
+
type: string
|
|
52
|
+
from:
|
|
53
|
+
type: string
|
|
54
|
+
to:
|
|
55
|
+
type: string
|
|
56
|
+
required:
|
|
57
|
+
- repository
|
|
58
|
+
|
|
59
|
+
activeagent:
|
|
60
|
+
class_name: ChangelogWriterAgent
|
|
61
|
+
concerns:
|
|
62
|
+
- has_tools: [list_merged_pulls]
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# Changelog Writer
|
|
66
|
+
|
|
67
|
+
You write release changelogs from merged pull requests.
|
|
68
|
+
|
|
69
|
+
## Instructions
|
|
70
|
+
|
|
71
|
+
1. Call `list_merged_pulls` for the requested range.
|
|
72
|
+
2. Group the changes into Added, Changed, Fixed and Removed.
|
|
73
|
+
3. Write one line per change, in the present tense, describing what a
|
|
74
|
+
reader can now do differently — not which files moved.
|
|
75
|
+
4. List anything that breaks an existing setup under `breaking`, with the
|
|
76
|
+
migration step spelled out.
|
|
77
|
+
5. Leave a category out entirely rather than padding it.
|
|
78
|
+
|
|
79
|
+
## Template
|
|
80
|
+
|
|
81
|
+
Write the changelog for {{ repository }} covering {{ from | default: "the last release" }} to {{ to | default: "HEAD" }}, for an audience of {{ audience | default: "users" }}.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Portable agent manifests — rails console walkthrough.
|
|
4
|
+
#
|
|
5
|
+
# A manifest is an agent definition that lives in a file instead of a class:
|
|
6
|
+
# frontmatter for the model, tools, input/output schemas and framework
|
|
7
|
+
# extensions, Markdown for the instructions. It can be reviewed in a pull
|
|
8
|
+
# request, shipped to another framework, or loaded into a running app.
|
|
9
|
+
#
|
|
10
|
+
# Docs: https://docs.activeagents.ai/solid_agent/manifests
|
|
11
|
+
# Format spec: https://github.com/activeagents/solid_agent/blob/main/docs/agent-md-spec.md
|
|
12
|
+
|
|
13
|
+
path = "examples/manifests/changelog_writer.agent.md"
|
|
14
|
+
|
|
15
|
+
# --- Read ---------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
manifest = SolidAgent::AgentManifest.parse(path)
|
|
18
|
+
manifest.name # => "changelog-writer"
|
|
19
|
+
manifest.model # => "anthropic/claude-sonnet-4-20250514"
|
|
20
|
+
manifest.tools.map(&:name)
|
|
21
|
+
manifest.instructions # the Markdown body
|
|
22
|
+
manifest.fingerprint # stable digest — the version an agent ran under
|
|
23
|
+
|
|
24
|
+
# `load` takes anything: a path, a URL, a JSON/YAML string, or a Hash.
|
|
25
|
+
SolidAgent::AgentManifest.load("https://example.com/agents/support.agent.md")
|
|
26
|
+
SolidAgent::AgentManifest.load({ name: "quick", model: "openai/gpt-4o-mini" })
|
|
27
|
+
|
|
28
|
+
# --- Validate -----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
SolidAgent::AgentManifest.validate(path) # => [] when valid
|
|
31
|
+
SolidAgent::AgentManifest.valid?(path) # => true
|
|
32
|
+
SolidAgent::AgentManifest.validate(path, strict: true)
|
|
33
|
+
SolidAgent::AgentManifest.validate!(path) # raises ValidationError
|
|
34
|
+
|
|
35
|
+
# Worth wiring into CI, so a broken manifest fails the build rather than a
|
|
36
|
+
# request:
|
|
37
|
+
Dir["config/agents/**/*.agent.md"].flat_map { |f| SolidAgent::AgentManifest.validate(f) }
|
|
38
|
+
|
|
39
|
+
# --- Build --------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
# Build an agent class from the manifest. The class arrives configured but
|
|
42
|
+
# not finished: it inherits from ApplicationAgent, includes the concerns
|
|
43
|
+
# the manifest asked for, carries the manifest's tool schemas, and keeps
|
|
44
|
+
# the model, provider and instructions as class attributes.
|
|
45
|
+
klass = SolidAgent::AgentManifest.load_agent(path, class_name: "ChangelogWriterAgent")
|
|
46
|
+
|
|
47
|
+
klass._manifest_provider # => "anthropic"
|
|
48
|
+
klass._manifest_model # => "claude-sonnet-4-20250514"
|
|
49
|
+
klass._manifest_instructions # the Markdown body
|
|
50
|
+
klass._manifest # the Manifest itself, fingerprint included
|
|
51
|
+
klass.new.tools.map { |t| t[:name] } # => ["list_merged_pulls"]
|
|
52
|
+
|
|
53
|
+
# `activeagent.class_name` in the frontmatter names the constant, so
|
|
54
|
+
# passing class_name: here is only needed to override it. Name it either
|
|
55
|
+
# way when persisting context — contexts are keyed by class name, and an
|
|
56
|
+
# anonymous class has none.
|
|
57
|
+
|
|
58
|
+
# What the manifest does not carry is behaviour: actions and tool bodies
|
|
59
|
+
# are still Ruby. Reopen the class and supply them.
|
|
60
|
+
#
|
|
61
|
+
# class ChangelogWriterAgent
|
|
62
|
+
# generate_with _manifest_provider.to_sym, model: _manifest_model
|
|
63
|
+
#
|
|
64
|
+
# def write
|
|
65
|
+
# prompt instructions: _manifest_instructions,
|
|
66
|
+
# message: params[:message],
|
|
67
|
+
# tools: tools
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# # Declared tools raise NotImplementedError until you define them.
|
|
71
|
+
# def list_merged_pulls(repository:, from: nil, to: nil)
|
|
72
|
+
# GitHub.merged_pulls(repository, from: from, to: to)
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# ChangelogWriterAgent.with(message: "Release 1.2.0").write.generate_now
|
|
77
|
+
|
|
78
|
+
# --- Convert ------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
SolidAgent::AgentManifest.parser_formats # what can be read
|
|
81
|
+
SolidAgent::AgentManifest.exporter_formats # what can be written
|
|
82
|
+
|
|
83
|
+
# Import someone else's definition...
|
|
84
|
+
SolidAgent::AgentManifest.parse("agents.yaml") # CrewAI
|
|
85
|
+
SolidAgent::AgentManifest.parse("basic.prompt") # Google Dotprompt
|
|
86
|
+
SolidAgent::AgentManifest.parse("copilot.prompt.md") # GitHub Copilot
|
|
87
|
+
|
|
88
|
+
# ...and export yours for them.
|
|
89
|
+
SolidAgent::AgentManifest.export(manifest, :dotprompt)
|
|
90
|
+
SolidAgent::AgentManifest.convert(path, :crewai, "tmp/agents.yaml")
|
|
91
|
+
|
|
92
|
+
# --- Provenance ---------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
# What an agent ran under, checksummed — pairs with the provenance
|
|
95
|
+
# HasContext records on every generation.
|
|
96
|
+
SolidAgent::AgentManifest.provenance(manifest)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# First half of a hand-off: an agent that researches a project and writes
|
|
4
|
+
# what it learned to long-term memory.
|
|
5
|
+
#
|
|
6
|
+
# Memory is scoped to (subject record, scope name) — not to the agent class
|
|
7
|
+
# — so anything this agent saves is readable by any other agent working on
|
|
8
|
+
# the same project. `save_memory` and `recall_memory` are ordinary
|
|
9
|
+
# function-calling tools, so the model decides when to write and when to
|
|
10
|
+
# read; you only decide what the subject is.
|
|
11
|
+
#
|
|
12
|
+
# Docs: https://docs.activeagents.ai/solid_agent/memory
|
|
13
|
+
class ResearcherAgent < ApplicationAgent
|
|
14
|
+
include SolidAgent::HasMemory
|
|
15
|
+
|
|
16
|
+
generate_with :openai, model: "gpt-4o-mini"
|
|
17
|
+
|
|
18
|
+
# scope: "default", class_name: "AgentMemory" unless you say otherwise.
|
|
19
|
+
# Use a scope to give one subject independent memory streams
|
|
20
|
+
# (has_memory scope: "competitive_research").
|
|
21
|
+
has_memory
|
|
22
|
+
|
|
23
|
+
def research
|
|
24
|
+
prompt(
|
|
25
|
+
message: "Research #{params[:project].name} and save what a writer would need to know.",
|
|
26
|
+
tools: memory_tool_definitions
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# memory_subject defaults to params[:memorable], falling back to the
|
|
31
|
+
# HasContext contextable. Override it when the subject is somewhere else
|
|
32
|
+
# — here the project the run is about.
|
|
33
|
+
def memory_subject
|
|
34
|
+
params[:project]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Second half of the hand-off: a different agent class, same subject.
|
|
4
|
+
#
|
|
5
|
+
# Two ways to pick up what ResearcherAgent left behind:
|
|
6
|
+
#
|
|
7
|
+
# 1. Give the model the recall tool and let it decide (below).
|
|
8
|
+
# 2. Prime the instructions with `memory.to_prompt` so the notes are in
|
|
9
|
+
# context from the first token — cheaper, and the model can't forget
|
|
10
|
+
# to look. `draft_with_primed_memory` does that.
|
|
11
|
+
#
|
|
12
|
+
# Docs: https://docs.activeagents.ai/solid_agent/memory
|
|
13
|
+
class WriterAgent < ApplicationAgent
|
|
14
|
+
include SolidAgent::HasMemory
|
|
15
|
+
|
|
16
|
+
generate_with :openai, model: "gpt-4o-mini"
|
|
17
|
+
|
|
18
|
+
has_memory
|
|
19
|
+
|
|
20
|
+
def draft
|
|
21
|
+
prompt(
|
|
22
|
+
message: "Draft the launch post for #{params[:project].name}.",
|
|
23
|
+
tools: memory_tool_definitions
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def draft_with_primed_memory
|
|
28
|
+
prompt(
|
|
29
|
+
instructions: [
|
|
30
|
+
"You are a product writer.",
|
|
31
|
+
memory&.to_prompt
|
|
32
|
+
].compact.join("\n\n"),
|
|
33
|
+
message: "Draft the launch post for #{params[:project].name}.",
|
|
34
|
+
tools: memory_tool_definitions
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def memory_subject
|
|
39
|
+
params[:project]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Memory hand-off between two agents — rails console walkthrough.
|
|
4
|
+
#
|
|
5
|
+
# Docs: https://docs.activeagents.ai/solid_agent/memory
|
|
6
|
+
|
|
7
|
+
project = Project.find(1)
|
|
8
|
+
|
|
9
|
+
# The researcher calls save_memory as it works. Each note records which
|
|
10
|
+
# agent class wrote it.
|
|
11
|
+
ResearcherAgent.with(project: project).research.generate_now
|
|
12
|
+
|
|
13
|
+
memory = AgentMemory.for(project)
|
|
14
|
+
memory.recall(limit: 5).map { |e| [ e.source_agent, e.category, e.content ] }
|
|
15
|
+
# => [["ResearcherAgent", "fact", "Ships on the 14th; pricing unchanged"], ...]
|
|
16
|
+
|
|
17
|
+
# A different agent class, later — possibly a different request, job, or
|
|
18
|
+
# deploy — picks the same subject up and reads those notes back.
|
|
19
|
+
WriterAgent.with(project: project).draft.generate_now
|
|
20
|
+
|
|
21
|
+
# Or hand the notes over without spending a tool call, by putting them in
|
|
22
|
+
# the instructions:
|
|
23
|
+
memory.to_prompt
|
|
24
|
+
# => "Memory notes for this subject:\n- Ships on the 14th... (ResearcherAgent)"
|
|
25
|
+
|
|
26
|
+
WriterAgent.with(project: project).draft_with_primed_memory.generate_now
|
|
27
|
+
|
|
28
|
+
# Scopes keep unrelated streams apart on the same subject:
|
|
29
|
+
AgentMemory.for(project, scope: "competitive_research").remember(
|
|
30
|
+
"Competitor X ships a similar feature in Q3",
|
|
31
|
+
source_agent: "MarketAgent",
|
|
32
|
+
category: "fact"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Categories filter recall; entries come back newest first.
|
|
36
|
+
memory.recall(category: "handoff", limit: 10)
|
|
37
|
+
|
|
38
|
+
# Curation is ordinary Active Record — nothing here is append-only by
|
|
39
|
+
# force, so prune when a note goes stale.
|
|
40
|
+
memory.entries.where(category: "task").find_each(&:destroy)
|
|
41
|
+
|
|
42
|
+
# The same tool contract is available to non-agent executors (a platform
|
|
43
|
+
# service, an MCP server) without including the concern:
|
|
44
|
+
SolidAgent::HasMemory.tool_definitions.map { |t| t[:name] }
|
|
45
|
+
# => ["save_memory", "recall_memory"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A support agent whose conversation outlives the request.
|
|
4
|
+
#
|
|
5
|
+
# `has_context :conversation, contextual: :user` persists the prompt, the
|
|
6
|
+
# assistant's reply, and the tool exchange in between to the tables the
|
|
7
|
+
# install generator created — agent_contexts, agent_messages and
|
|
8
|
+
# agent_generations — keyed by the record passed as params[:user].
|
|
9
|
+
#
|
|
10
|
+
# The macro defines the methods used below (load_conversation,
|
|
11
|
+
# conversation_messages, conversation_result, conversation_summary, ...).
|
|
12
|
+
# Name the context something else and the methods rename with it.
|
|
13
|
+
#
|
|
14
|
+
# Docs: https://docs.activeagents.ai/solid_agent/context
|
|
15
|
+
class SupportAgent < ApplicationAgent
|
|
16
|
+
include SolidAgent::HasContext
|
|
17
|
+
|
|
18
|
+
generate_with :openai, model: "gpt-4o-mini"
|
|
19
|
+
|
|
20
|
+
# Naming a context also names the models it resolves: :conversation alone
|
|
21
|
+
# would look for Conversation / ConversationMessage / ConversationGeneration
|
|
22
|
+
# and raise NameError on the first request. class_name points it back at
|
|
23
|
+
# what `solid_agent:install` wrote (AgentMessage and AgentGeneration are
|
|
24
|
+
# inferred from it). Want separate tables instead? Run
|
|
25
|
+
# `rails generate solid_agent:context conversation` and drop class_name.
|
|
26
|
+
has_context :conversation, class_name: "AgentContext", contextual: :user
|
|
27
|
+
|
|
28
|
+
# Multi-turn: load the stored conversation, append this turn's question,
|
|
29
|
+
# and send the whole history as the prompt.
|
|
30
|
+
#
|
|
31
|
+
# Nothing here writes to the database. With auto_save on (the default),
|
|
32
|
+
# SolidAgent persists the last prompt message as the user turn and the
|
|
33
|
+
# response as the assistant turn, both after the provider call — so the
|
|
34
|
+
# next request replays this exchange. Add messages by hand only with
|
|
35
|
+
# `auto_save: false`, or the turn is stored twice.
|
|
36
|
+
#
|
|
37
|
+
# `contextual: :user` alone would create the context for you, but it runs
|
|
38
|
+
# after the prompt is built — a conversation has to be loaded *before*
|
|
39
|
+
# that to replay its messages, so load it explicitly here.
|
|
40
|
+
def answer
|
|
41
|
+
load_conversation(contextable: params[:user])
|
|
42
|
+
|
|
43
|
+
prompt messages: conversation_messages + [
|
|
44
|
+
{ role: "user", content: params[:message] }
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Contexts are keyed by (contextable, agent_name, action_name), so a
|
|
49
|
+
# second action gets its own row rather than appending to the chat
|
|
50
|
+
# history. To work against an existing conversation from a different
|
|
51
|
+
# entry point, load it by id instead of by contextable.
|
|
52
|
+
def summarize
|
|
53
|
+
load_conversation(context_id: params[:conversation_id])
|
|
54
|
+
|
|
55
|
+
prompt messages: conversation_messages + [
|
|
56
|
+
{ role: "user", content: "Summarize this conversation in three bullet points." }
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The web side of a persisted conversation: one POST per turn, and a show
|
|
4
|
+
# action that renders the history straight out of the database — no session
|
|
5
|
+
# state, no cache, nothing to warm up after a deploy.
|
|
6
|
+
class SupportConversationsController < ApplicationController
|
|
7
|
+
def show
|
|
8
|
+
@conversation = AgentContext
|
|
9
|
+
.for_agent("SupportAgent")
|
|
10
|
+
.for_action("answer")
|
|
11
|
+
.find_by!(contextable: current_user)
|
|
12
|
+
|
|
13
|
+
@messages = @conversation.messages.chronological
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
response = SupportAgent.with(
|
|
18
|
+
user: current_user,
|
|
19
|
+
message: params.require(:message)
|
|
20
|
+
).answer.generate_now
|
|
21
|
+
|
|
22
|
+
render json: { reply: response.message.content }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
You are a support agent.
|
|
2
|
+
|
|
3
|
+
Answer in two short paragraphs at most. When you do not know something, say
|
|
4
|
+
so and name the next step the customer should take rather than guessing.
|
|
5
|
+
|
|
6
|
+
<%# Instructions are always rendered from this template. The action
|
|
7
|
+
template (answer.md.erb) is only a fallback for the message — pass
|
|
8
|
+
`messages:` to prompt, as SupportAgent#answer does, and it is skipped. %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Persistent conversation — rails console walkthrough.
|
|
4
|
+
#
|
|
5
|
+
# Docs: https://docs.activeagents.ai/solid_agent/context
|
|
6
|
+
|
|
7
|
+
user = User.first
|
|
8
|
+
|
|
9
|
+
# Turn one. The context row, the user message and the assistant message are
|
|
10
|
+
# all written during generate_now.
|
|
11
|
+
SupportAgent.with(user: user, message: "My invoice is wrong").answer.generate_now
|
|
12
|
+
|
|
13
|
+
# Turn two. The agent replays turn one from the database before asking.
|
|
14
|
+
SupportAgent.with(user: user, message: "It's the VAT line").answer.generate_now
|
|
15
|
+
|
|
16
|
+
conversation = AgentContext.for_agent("SupportAgent").find_by(contextable: user)
|
|
17
|
+
|
|
18
|
+
conversation.messages.chronological.map { |m| [ m.role, m.content ] }
|
|
19
|
+
# => [["user", "My invoice is wrong"],
|
|
20
|
+
# ["assistant", "..."],
|
|
21
|
+
# ["user", "It's the VAT line"],
|
|
22
|
+
# ["assistant", "..."]]
|
|
23
|
+
|
|
24
|
+
conversation.total_tokens # cumulative across both turns
|
|
25
|
+
conversation.generations.count # => 2, one row per provider call
|
|
26
|
+
|
|
27
|
+
# Every generation carries what produced it: model, finish reason, token
|
|
28
|
+
# split, duration, raw provider payload, and a provenance snapshot of the
|
|
29
|
+
# agent/prompt/context checksums at the time.
|
|
30
|
+
generation = conversation.generations.last
|
|
31
|
+
generation.model # => "gpt-4o-mini"
|
|
32
|
+
generation.total_tokens
|
|
33
|
+
generation.estimated_cost # => USD estimate via SolidAgent::ModelPricing
|
|
34
|
+
generation.provenance["agent_checksum"]
|
|
35
|
+
|
|
36
|
+
# Thread a distributed trace id through prompt_options and the generation
|
|
37
|
+
# joins up with your telemetry:
|
|
38
|
+
#
|
|
39
|
+
# def answer
|
|
40
|
+
# prompt_options[:trace_id] = Current.trace_id
|
|
41
|
+
# ...
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
AgentGeneration.with_trace("some-trace-id")
|
|
45
|
+
AgentContext.with_trace("some-trace-id")
|
|
46
|
+
|
|
47
|
+
# Reading a conversation back for a UI is plain Active Record — the
|
|
48
|
+
# generated models are yours, scopes included.
|
|
49
|
+
conversation.messages.assistant_messages.last&.content
|
|
50
|
+
AgentContext.for_agent("SupportAgent").recent.limit(10)
|
|
51
|
+
user.agent_contexts if user.respond_to?(:agent_contexts) # add the has_many yourself
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Capturing extended thinking.
|
|
4
|
+
#
|
|
5
|
+
# Models that expose reasoning (Claude's extended thinking, OpenAI's
|
|
6
|
+
# reasoning models) return it alongside the answer. HasReasons collects it
|
|
7
|
+
# on the agent instance and, with `persist: true`, writes it onto the
|
|
8
|
+
# generation record so it survives the request — as long as that model
|
|
9
|
+
# includes SolidAgent::Reasonable and has the columns:
|
|
10
|
+
#
|
|
11
|
+
# rails generate solid_agent:reasons AgentGeneration
|
|
12
|
+
# rails db:migrate
|
|
13
|
+
#
|
|
14
|
+
# Reasoning is model output about its own process: treat it as sensitive.
|
|
15
|
+
# `redact_on_persist: true` keeps the token counts and drops the text.
|
|
16
|
+
#
|
|
17
|
+
# Docs: https://docs.activeagents.ai/solid_agent/reasoning
|
|
18
|
+
class AnalysisAgent < ApplicationAgent
|
|
19
|
+
include SolidAgent::HasContext
|
|
20
|
+
include SolidAgent::HasReasons
|
|
21
|
+
|
|
22
|
+
generate_with :anthropic, model: "claude-sonnet-5"
|
|
23
|
+
|
|
24
|
+
# Reasoning is read off the response, so it has to be handed to
|
|
25
|
+
# capture_reasoning once the provider has answered. Declared *before*
|
|
26
|
+
# has_context on purpose: around callbacks nest in declaration order, so
|
|
27
|
+
# this one wraps HasContext's — and by the time it runs, the generation
|
|
28
|
+
# row that `persist: true` updates has been written.
|
|
29
|
+
around_generation :capture_generation_reasoning
|
|
30
|
+
|
|
31
|
+
has_context contextual: :document
|
|
32
|
+
|
|
33
|
+
has_reasons auto_capture: true, # reasoning_prompt_options asks for thinking
|
|
34
|
+
persist: true, # store on the generation record
|
|
35
|
+
budget_tokens: 10_000, # default thinking budget
|
|
36
|
+
redact_on_persist: false # true stores "[Redacted]" + tokens
|
|
37
|
+
|
|
38
|
+
def analyze
|
|
39
|
+
prompt(
|
|
40
|
+
message: "What risks does this contract create for the buyer?",
|
|
41
|
+
**reasoning_prompt_options # extended_thinking + budget from has_reasons
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def capture_generation_reasoning
|
|
48
|
+
response = yield
|
|
49
|
+
capture_reasoning(response)
|
|
50
|
+
response
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reasoning capture — rails console walkthrough.
|
|
4
|
+
#
|
|
5
|
+
# Docs: https://docs.activeagents.ai/solid_agent/reasoning
|
|
6
|
+
|
|
7
|
+
document = Document.find(1)
|
|
8
|
+
|
|
9
|
+
AnalysisAgent.with(document: document).analyze.generate_now
|
|
10
|
+
|
|
11
|
+
# Persisted (persist: true + SolidAgent::Reasonable on the model). This is
|
|
12
|
+
# what survives the request:
|
|
13
|
+
generation = AgentGeneration.recent.first
|
|
14
|
+
generation.reasoning_content
|
|
15
|
+
generation.reasoning_tokens
|
|
16
|
+
generation.has_reasoning?
|
|
17
|
+
generation.reasoning_summary(length: 120)
|
|
18
|
+
generation.thinking? # reasoning_tokens > 0 on the generated model
|
|
19
|
+
|
|
20
|
+
# In-memory, on the agent instance that ran — reachable from inside an
|
|
21
|
+
# action or a callback, not from the console after the fact:
|
|
22
|
+
#
|
|
23
|
+
# def analyze
|
|
24
|
+
# prompt(...)
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def after_response
|
|
28
|
+
# reasons # => [SolidAgent::Reasonable::Reason, ...]
|
|
29
|
+
# last_reasoning&.content
|
|
30
|
+
# total_reasoning_tokens
|
|
31
|
+
# has_reasoning?
|
|
32
|
+
# reasoning_chain # every non-redacted reason, joined
|
|
33
|
+
# reasoning_stats
|
|
34
|
+
# # => { count: 2, total_tokens: 450, total_thinking_time_ms: 1200,
|
|
35
|
+
# # redacted_count: 0, models: ["claude-sonnet-5"] }
|
|
36
|
+
# end
|
|
37
|
+
|
|
38
|
+
# Storing reasoning by hand — from a provider response, or as a note:
|
|
39
|
+
generation.store_reasoning!(response)
|
|
40
|
+
generation.store_reason!(
|
|
41
|
+
SolidAgent::Reasonable::Reason.new(content: "Chose the strict parser", tokens: 0)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Reasoning columns on any generation-shaped model:
|
|
45
|
+
#
|
|
46
|
+
# rails generate solid_agent:reasons MyGeneration \
|
|
47
|
+
# --content_column thinking_trace --tokens_column think_tokens
|
|
48
|
+
#
|
|
49
|
+
# class MyGeneration < ApplicationRecord
|
|
50
|
+
# include SolidAgent::Reasonable
|
|
51
|
+
# reasonable_config column: :thinking_trace, tokens_column: :think_tokens
|
|
52
|
+
# end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The agent the run records. Two details matter for run tracking:
|
|
4
|
+
#
|
|
5
|
+
# - the instructions are a constant, so the executor can fingerprint the
|
|
6
|
+
# exact text a run executed under (see DocumentAnalysisRun),
|
|
7
|
+
# - the caller's trace id is threaded into prompt_options, so the
|
|
8
|
+
# generation row, the context row and the run all carry the same
|
|
9
|
+
# trace_id and can be joined with your telemetry.
|
|
10
|
+
#
|
|
11
|
+
# Docs: https://docs.activeagents.ai/solid_agent/runs
|
|
12
|
+
class ReportAgent < ApplicationAgent
|
|
13
|
+
include SolidAgent::HasContext
|
|
14
|
+
|
|
15
|
+
INSTRUCTIONS = <<~TEXT.freeze
|
|
16
|
+
You are a document analyst. Answer only from the document you are given,
|
|
17
|
+
quote the clause you are relying on, and say plainly when the document
|
|
18
|
+
does not cover the question.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
generate_with :openai, model: "gpt-4o-mini"
|
|
22
|
+
|
|
23
|
+
has_context contextual: :document
|
|
24
|
+
|
|
25
|
+
def analyze
|
|
26
|
+
prompt_options[:trace_id] = params[:trace_id] if params[:trace_id]
|
|
27
|
+
|
|
28
|
+
prompt instructions: INSTRUCTIONS, message: params[:question]
|
|
29
|
+
end
|
|
30
|
+
end
|