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();