diff --git a/.embabel/coding-style.md b/.embabel/coding-style.md
new file mode 100644
index 0000000..45ae8b5
--- /dev/null
+++ b/.embabel/coding-style.md
@@ -0,0 +1,34 @@
+# Coding style
+
+## General
+
+The project uses Maven, Kotlin and Spring Boot.
+
+Follow the style of the code you read. Favor clarity.
+
+Don't bother putting in licence headers, as build will do that.
+
+Don't comment obvious things, inline or in type headers.
+Comment only things that may be non-obvious. LLMs offer comment
+more than humans; don't do that.
+
+Use consistent naming in the Spring idiom.
+
+Use the Spring idiom where possible.
+
+Favor immutability.
+
+Use the `Schema` and related annotations to add information to types passed over the wire in the REST application.
+This will improve Swagger/OpenAPI documentation.
+
+Unless there is a specific reason not to, use the latest GA version of all dependencies.
+
+Use @Nested classes in tests. Use `test complicated thing` instead of @DisplayName for test cases.
+
+In log statements, use placeholders for efficiency at all logging levels.
+E.g. logger.info("{} {}", a, b) instead of logger.info("computed string").
+
+## Java
+
+- Use modern Java features like var, records, and enhanced switch expressions.
+- Use multiline strings rather than concatenation.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0ddfee2..79d9f7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
21
- 0.1.1
+ 0.1.2-SNAPSHOT
@@ -30,11 +30,22 @@
${embabel-agent.version}
+
+ com.embabel.agent
+ embabel-agent-code
+ ${embabel-agent.version}
+
+
+
+ com.embabel.agent
+ embabel-agent-rag-lucene
+ ${embabel-agent.version}
+
+
com.embabel.agent
embabel-agent-test
-
- 0.1.2-SNAPSHOT
+ ${embabel-agent.version}
test
@@ -61,6 +72,9 @@
embabel-releases
https://repo.embabel.com/artifactory/libs-release
+
+ true
+
false
@@ -68,13 +82,11 @@
embabel-snapshots
https://repo.embabel.com/artifactory/libs-snapshot
-
- false
-
true
+
diff --git a/src/main/java/com/embabel/template/DemoShell.java b/src/main/java/com/embabel/DemoShell.java
similarity index 92%
rename from src/main/java/com/embabel/template/DemoShell.java
rename to src/main/java/com/embabel/DemoShell.java
index c5976e0..23264e3 100644
--- a/src/main/java/com/embabel/template/DemoShell.java
+++ b/src/main/java/com/embabel/DemoShell.java
@@ -1,4 +1,4 @@
-package com.embabel.template;
+package com.embabel;
import com.embabel.template.injected.InjectedDemo;
import org.springframework.shell.standard.ShellComponent;
diff --git a/src/main/java/com/embabel/ProjectNameApplication.java b/src/main/java/com/embabel/ProjectNameApplication.java
index 9f89218..3a68dfb 100644
--- a/src/main/java/com/embabel/ProjectNameApplication.java
+++ b/src/main/java/com/embabel/ProjectNameApplication.java
@@ -18,12 +18,15 @@
import com.embabel.agent.config.annotation.EnableAgentShell;
import com.embabel.agent.config.annotation.EnableAgents;
import com.embabel.agent.config.annotation.LoggingThemes;
+import com.embabel.coding.Tyrell;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableAgentShell
+@EnableConfigurationProperties(Tyrell.Config.class)
@EnableAgents(loggingTheme = LoggingThemes.STAR_WARS)
class ProjectNameApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/embabel/coding/ReferenceConfiguration.java b/src/main/java/com/embabel/coding/ReferenceConfiguration.java
new file mode 100644
index 0000000..f36e79c
--- /dev/null
+++ b/src/main/java/com/embabel/coding/ReferenceConfiguration.java
@@ -0,0 +1,18 @@
+package com.embabel.coding;
+
+import com.embabel.agent.rag.RagService;
+import com.embabel.agent.rag.lucene.LuceneRagService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ReferenceConfiguration {
+
+ @Bean
+ public RagService ragService() {
+ return new LuceneRagService(
+ "code ref",
+ "code description"
+ );
+ }
+}
diff --git a/src/main/java/com/embabel/coding/Tyrell.java b/src/main/java/com/embabel/coding/Tyrell.java
new file mode 100644
index 0000000..7d5d6da
--- /dev/null
+++ b/src/main/java/com/embabel/coding/Tyrell.java
@@ -0,0 +1,91 @@
+package com.embabel.coding;
+
+import com.embabel.agent.api.annotation.*;
+import com.embabel.agent.api.common.LlmReference;
+import com.embabel.agent.api.common.OperationContext;
+import com.embabel.agent.domain.library.code.SoftwareProject;
+import com.embabel.agent.spi.ToolGroupResolver;
+import com.embabel.coding.tools.api.ApiReference;
+import com.embabel.coding.tools.git.RepositoryReferenceProvider;
+import com.embabel.coding.tools.jvm.ClassGraphApiReferenceExtractor;
+import com.embabel.common.ai.model.LlmOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+@Agent(description = "Agent that creates other agents")
+public class Tyrell {
+
+ private final Config config;
+ private final ToolGroupResolver toolGroupResolver;
+
+ private final List references = new LinkedList<>();
+
+ private final SoftwareProject softwareProject = new SoftwareProject(System.getProperty("user.dir"));
+
+ public Tyrell(Config config, ToolGroupResolver toolGroupResolver) {
+ this.config = config;
+ this.toolGroupResolver = toolGroupResolver;
+
+ var embabelApiReference = new ApiReference(
+ new ClassGraphApiReferenceExtractor().fromProjectClasspath(
+ "embabel-agent",
+ Set.of("com.embabel.agent"),
+ Set.of()),
+ 100);
+ var examplesReference = RepositoryReferenceProvider.create()
+ .cloneRepository("https://github.com/embabel/embabel-agent-examples.git");
+ references.add(embabelApiReference);
+ references.add(examplesReference);
+ references.add(softwareProject);
+ }
+
+
+ @ConfigurationProperties(prefix = "embabel.tyrell")
+ public record Config(LlmOptions codingLlm) {
+ }
+
+ public record AgentCreationRequest(String pkg, String purpose) {
+ }
+
+ /**
+ * The agent will have been created to fulfill this summary.
+ *
+ * @param summary
+ */
+ public record AgentCreationResult(String summary) {
+ }
+
+ public record ToolGroups(List toolGroups) {
+ }
+
+ @Action
+ AgentCreationRequest requestAgentDetails() {
+ return WaitFor.formSubmission(
+ "Please enter the package and purpose of the new agent",
+ AgentCreationRequest.class);
+ }
+
+ @Action
+ @AchievesGoal(
+ description = "New agent has been created",
+ export = @Export(remote = true, startingInputTypes = {AgentCreationRequest.class}))
+ public AgentCreationResult createAgent(AgentCreationRequest request, OperationContext embabel) {
+ return embabel.ai()
+ .withLlm(config.codingLlm)
+ .withReferences(references)
+ .withTemplate("coding/creator")
+ .createObject(
+ AgentCreationResult.class,
+ Map.of(
+ "thisProject", softwareProject.getName().replace("-", "_"),
+ "package", request.pkg(),
+ "purpose", request.purpose(),
+ "toolGroups", toolGroupResolver.availableToolGroups()
+ ));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 59bee9a..80e7e60 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,3 +9,5 @@
#embabel.models.embeddingServices.cheapest=nomic-embed-text:latest
#
#embabel.agent-platform.ranking.llm=llama3.1:8b
+embabel.tyrell.coding-llm.model=claude-3-7-sonnet-latest
+#embabel.tyrell.coding-llm.model=claude-3-5-haiku-latest
diff --git a/src/main/resources/prompts/.gitkeep b/src/main/resources/prompts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/prompts/coding/creator.jinja b/src/main/resources/prompts/coding/creator.jinja
new file mode 100644
index 0000000..67e5fed
--- /dev/null
+++ b/src/main/resources/prompts/coding/creator.jinja
@@ -0,0 +1,48 @@
+Create a new Embabel agent in Java within this project.
+This project uses normal Maven conventions.
+
+Choose an appropriate class name within the package.
+Refer to API documentation and examples for how to build an agent.
+
+Start by considering the data flow between the agent's @Action methods.
+These types should be captured in records.
+
+For each @Action method, consider what tool groups may be required.
+Be parsimonious in your choice of tool groups, including only what's necessary.
+The available tool groups are the following (use the role string in code for withToolGroup):
+Role | Description
+---- | -----------
+{% for group in toolGroups %}
+ {{ group.role }} | {{ group.description }}
+{% endfor %}
+
+Ue modern Java with vars and records.
+
+Use the build tool to check that your build works and fix it if it doesn't.
+
+{#You should also create the following artifacts:#}
+{##}
+{#1. Unit test for the new agent. Use WriteAndReviewAgentTest in this repository as a guide#}
+
+{#2. Integration test for the new agent. Use#}
+
+DO NOT MODIFY ANY FILES BESIDES THE NEW FILES YOU'VE CREATED
+
+DO NOT MODIFY THE pom.xml
+
+Tools provided will give you access to reference repositories.
+The tool names beginning with "{{ thisProject }}" will give you access to this repository
+
+This project uses normal Maven project structure, you will add source code under src/main/java and tests under src/test/java.
+Note that the examples repo has a different structure.
+
+Create the simplest possible implementation that meets the requirements.
+Do not invent extra steps or actions.
+
+# PACKAGE
+
+The package should be {{ package }}
+
+# PURPOSE OF THE AGENT
+
+{{ purpose }}
\ No newline at end of file
diff --git a/src/test/java/com/embabel/joke/agent/JokeAgentTest.java b/src/test/java/com/embabel/joke/agent/JokeAgentTest.java
new file mode 100644
index 0000000..f7258eb
--- /dev/null
+++ b/src/test/java/com/embabel/joke/agent/JokeAgentTest.java
@@ -0,0 +1,130 @@
+package com.embabel.joke.agent;
+
+import com.embabel.agent.domain.io.UserInput;
+import com.embabel.agent.testing.unit.FakeOperationContext;
+import com.embabel.agent.testing.unit.FakePromptRunner;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class JokeAgentTest {
+
+ @Nested
+ class BrainstormJokeTests {
+ @Test
+ void testBrainstormJokeWithSpecificTopic() {
+ var context = FakeOperationContext.create();
+ var expectedIdea = new JokeIdea("cats", "pun");
+ context.expectResponse(expectedIdea);
+
+ var agent = new JokeAgent(150);
+ var result = agent.brainstormJoke(new UserInput("Tell me a joke about cats", Instant.now()), context);
+
+ assertEquals("cats", result.topic());
+ assertEquals("pun", result.style());
+
+ var llmInvocation = context.getLlmInvocations().getFirst();
+ assertTrue(llmInvocation.getPrompt().contains("cats"), "Expected prompt to contain 'cats'");
+ }
+
+ @Test
+ void testBrainstormJokeWithNoSpecificTopic() {
+ var context = FakeOperationContext.create();
+ context.expectResponse(new JokeIdea("coffee", "observational"));
+
+ var agent = new JokeAgent(150);
+ var result = agent.brainstormJoke(new UserInput("Tell me a funny joke", Instant.now()), context);
+
+ assertNotNull(result.topic());
+ assertNotNull(result.style());
+ }
+ }
+
+ @Nested
+ class WriteJokeTests {
+ @Test
+ void testWriteJokeFromIdea() {
+ var context = FakeOperationContext.create();
+ var promptRunner = (FakePromptRunner) context.promptRunner();
+ var expectedJoke = new Joke("Why don't cats play poker?", "Too many cheetahs!");
+ context.expectResponse(expectedJoke);
+
+ var agent = new JokeAgent(150);
+ var jokeIdea = new JokeIdea("cats", "pun");
+ var result = agent.writeJoke(jokeIdea, context);
+
+ assertNotNull(result.setup());
+ assertNotNull(result.punchline());
+ assertEquals("Why don't cats play poker?", result.setup());
+ assertEquals("Too many cheetahs!", result.punchline());
+
+ var prompt = promptRunner.getLlmInvocations().getFirst().getPrompt();
+ assertTrue(prompt.contains("cats"), "Expected prompt to contain topic");
+ assertTrue(prompt.contains("pun"), "Expected prompt to contain style");
+ }
+ }
+
+ @Nested
+ class RefineJokeTests {
+ @Test
+ void testRefineJokeWithCritique() {
+ var agent = new JokeAgent(150);
+ var userInput = new UserInput("Tell me a joke about programming", Instant.now());
+ var originalJoke = new Joke("Why do programmers prefer dark mode?", "Because light attracts bugs!");
+ var context = FakeOperationContext.create();
+
+ // First expectation for critique
+ context.expectResponse("Good pun but could be more clever. Consider a twist on the setup.");
+
+ // Second expectation for improved joke
+ var improvedJoke = new Joke("Why do programmers hate nature?", "It has too many bugs!");
+ context.expectResponse(improvedJoke);
+
+ var result = agent.refineJoke(userInput, originalJoke, context);
+
+ assertNotNull(result.critique());
+ assertNotNull(result.improvedJoke());
+ assertEquals(originalJoke, result.originalJoke());
+ assertTrue(result.critique().contains("pun"), "Expected critique to mention pun");
+
+ var llmInvocations = context.getLlmInvocations();
+ assertEquals(2, llmInvocations.size());
+
+ // Check critique prompt
+ var critiquePrompt = llmInvocations.get(0).getPrompt();
+ assertTrue(critiquePrompt.contains("Evaluate"), "Expected critique prompt to contain 'Evaluate'");
+ assertTrue(critiquePrompt.contains(originalJoke.setup()), "Expected critique prompt to contain original setup");
+
+ // Check improvement prompt
+ var improvementPrompt = llmInvocations.get(1).getPrompt();
+ assertTrue(improvementPrompt.contains("improved version"), "Expected improvement prompt to contain 'improved version'");
+ }
+ }
+
+ @Test
+ void testFullJokeGenerationFlow() {
+ var agent = new JokeAgent(200);
+ var userInput = new UserInput("I need a knock-knock joke", Instant.now());
+ var context = FakeOperationContext.create();
+
+ // Brainstorm
+ context.expectResponse(new JokeIdea("doors", "knock-knock"));
+ var jokeIdea = agent.brainstormJoke(userInput, context);
+
+ // Write joke
+ var joke = new Joke("Knock knock. Who's there? Interrupting cow.", "Interrupting cow w-- MOO!");
+ context.expectResponse(joke);
+ var writtenJoke = agent.writeJoke(jokeIdea, context);
+
+ // Refine joke
+ context.expectResponse("Classic format but timing could be better emphasized.");
+ context.expectResponse(new Joke("Knock knock. Who's there? Interrupting cow.", "Interrupt-- MOOOOO!"));
+ var refinedJoke = agent.refineJoke(userInput, writtenJoke, context);
+
+ assertNotNull(refinedJoke.improvedJoke());
+ assertEquals(4, context.getLlmInvocations().size());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/embabel/template/agent/WriteAndReviewAgentIntegrationTest.java b/src/test/java/com/embabel/template/agent/WriteAndReviewAgentIntegrationTest.java
deleted file mode 100644
index a3ece1e..0000000
--- a/src/test/java/com/embabel/template/agent/WriteAndReviewAgentIntegrationTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.embabel.template.agent;
-
-import com.embabel.agent.api.common.autonomy.AgentInvocation;
-import com.embabel.agent.domain.io.UserInput;
-import com.embabel.agent.testing.integration.EmbabelMockitoIntegrationTest;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.contains;
-
-/**
- * Use framework superclass to test the complete workflow of writing and reviewing a story.
- * This will run under Spring Boot against an AgentPlatform instance
- * that has loaded all our agents.
- */
-class WriteAndReviewAgentIntegrationTest extends EmbabelMockitoIntegrationTest {
-
- @Test
- void shouldExecuteCompleteWorkflow() {
- var input = new UserInput("Write about artificial intelligence");
-
- var story = new Story("AI will transform our world...");
- var reviewedStory = new ReviewedStory(story, "Excellent exploration of AI themes.", Personas.REVIEWER);
-
- whenCreateObject(contains("Craft a short story"), Story.class)
- .thenReturn(story);
-
- // The second call uses generateText
- whenGenerateText(contains("You will be given a short story to review"))
- .thenReturn(reviewedStory.review());
-
- var invocation = AgentInvocation.create(agentPlatform, ReviewedStory.class);
- var reviewedStoryResult = invocation.invoke(input);
-
- assertNotNull(reviewedStoryResult);
- assertTrue(reviewedStoryResult.getContent().contains(story.text()),
- "Expected story content to be present: " + reviewedStoryResult.getContent());
- assertEquals(reviewedStory, reviewedStoryResult,
- "Expected review to match: " + reviewedStoryResult);
-
- verifyCreateObjectMatching(prompt -> prompt.contains("Craft a short story"), Story.class,
- llm -> llm.getLlm().getTemperature() == 0.7 && llm.getToolGroups().isEmpty());
- verifyGenerateTextMatching(prompt -> prompt.contains("You will be given a short story to review"));
- verifyNoMoreInteractions();
- }
-}
diff --git a/src/test/java/com/embabel/template/agent/WriteAndReviewAgentTest.java b/src/test/java/com/embabel/template/agent/WriteAndReviewAgentTest.java
index b37485a..3b5c825 100644
--- a/src/test/java/com/embabel/template/agent/WriteAndReviewAgentTest.java
+++ b/src/test/java/com/embabel/template/agent/WriteAndReviewAgentTest.java
@@ -3,16 +3,14 @@
import com.embabel.agent.domain.io.UserInput;
import com.embabel.agent.testing.unit.FakeOperationContext;
import com.embabel.agent.testing.unit.FakePromptRunner;
-import com.embabel.agent.testing.unit.UnitTestUtils;
import org.junit.jupiter.api.Test;
import java.time.Instant;
-import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class WriteAndReviewAgentTest {
-
+
@Test
void testWriteAndReviewAgent() {
var context = FakeOperationContext.create();