diff --git a/.gitignore b/.gitignore index 0c936630..20ce30f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea **/.DS_Store **/.direnv/ /.vscode/ diff --git a/akka/.gitignore b/akka/.gitignore new file mode 100644 index 00000000..ee44a963 --- /dev/null +++ b/akka/.gitignore @@ -0,0 +1,2 @@ +.idea +target diff --git a/akka/LICENSE b/akka/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/akka/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/akka/Makefile b/akka/Makefile new file mode 100644 index 00000000..7664e8fd --- /dev/null +++ b/akka/Makefile @@ -0,0 +1,8 @@ + +OPENAI_API_KEY := $(shell cat secret.openai-api-key) +export OPENAI_API_KEY + +build-and-run: + docker rmi -f helloworld-agent:latest + mvn clean install -DskipTests -Pstandalone + docker compose up \ No newline at end of file diff --git a/akka/README.md b/akka/README.md new file mode 100644 index 00000000..8148d191 --- /dev/null +++ b/akka/README.md @@ -0,0 +1,108 @@ +# ๐Ÿง  Akka SDK - AI Agent Demo + +This project demonstrates how to build a chat agent using the [AI Agent component](https://doc.akka.io/java/agents.html) of the [Akka SDK](https://doc.akka.io/). + +We use OpenAI as the LLM provider. + +The following components are used: + +- `HttpEndpoint` to serve the UI and to process http requests. +- `Agent` with a system message to set the context for the prompt. + - Durable storage session based on the username. + - Streaming interaction with the LLM. + +

+ Akka SDK AI Agent +

+ +# ๐Ÿš€ Getting Started + +### Requirements + ++ Java 21 ++ [Maven] ++ **[Docker Desktop] 4.43.0+ or [Docker Engine]** installed. ++ **A laptop or workstation** (e.g., a MacBook). ++ If you're using Docker Engine on Linux, ensure you have [Docker Compose] 2.38.1 or later installed. ++ An [OpenAI API Key](https://platform.openai.com/api-keys) ๐Ÿ”‘. + +### Run the project + +Create a `secret.openai-api-key` file with your OpenAI API key: + +```plaintext +sk-... +``` + +Then run: + +```sh +make build-and-run +``` + +Alternatively, you can run: +```sh +export OPENAI_API_KEY= +mvn clean install -DskipTests -Pstandalone +docker compose up +``` + +Everything runs from the container. Open `http://localhost:9000` in your browser to start to interact with the agent. + +# ๐Ÿ“š More + +This demo was created as part of a [webinar we did](https://akka.io/blog/webinar-creating-certainty-in-the-age-of-agentic-ai). + +Feel free to take a look if you want to go further. The app was +- Created using vibe coding using Claude Desktop. +- Exposing MCP endpoints. +- Run locally with multiple instances forming a resilient cluster. + +To learn more, please visit our [getting started docs](https://doc.akka.io/getting-started/index.html) or check out [additional samples](https://github.com/akka-samples). + +# ๐Ÿงน Cleanup + +To stop and remove containers and volumes: + +```sh +docker compose down -v +``` + +# ๐Ÿ”ง Architecture Overview + +```mermaid +flowchart TD +subgraph Browser +localhost:9000 +end + + subgraph Akka SDK + HttpUi[๐Ÿ–ฅ๏ธ UI] + HttpAgent[๐ŸŽฏ Agent http api] + Agent[๐Ÿง  Agent: session memory] + end + + subgraph Open AI + LLM[(gpt-4o-mini)] + end + + Browser -->|Http| HttpUi + Browser -->|Http| HttpAgent + HttpAgent -->|Akka ComponentClient| Agent + Agent -->|Streaming| LLM +``` + +๐Ÿ“Ž Credits + ++ [Akka SDK Team] ++ [Akks SDK samples](https://github.com/akka-samples) ++ [Docker Compose] + +[Maven]: http://maven.apache.org/ +[Akka SDK Team]: https://docs.akka.io/ +[Docker Compose]: https://github.com/docker/compose +[Docker Desktop]: https://www.docker.com/products/docker-desktop/ +[Docker Engine]: https://docs.docker.com/engine/ \ No newline at end of file diff --git a/akka/compose.yml b/akka/compose.yml new file mode 100644 index 00000000..6c77a150 --- /dev/null +++ b/akka/compose.yml @@ -0,0 +1,39 @@ +services: + postgres-db: + image: postgres:latest + container_name: postgres_db + ports: + - 5432:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + healthcheck: + test: [ 'CMD', 'pg_isready', "-q", "-d", "postgres", "-U", "postgres" ] + interval: 5s + retries: 5 + start_period: 5s + timeout: 5s + + helloworld-agent: + image: helloworld-agent:latest + container_name: helloworld-agent + depends_on: + - postgres-db + ports: + - "9000:9000" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${HOME}/.akka/local:/root/.akka/local + - ./poststart.sh:/poststart.sh + environment: + # jvm -D properties can be added under this environment map (note: remove this comment when adding properties) + #JAVA_TOOL_OPTIONS: -Dconfig.resource=/root/.akka/local/api-key.conf + STANDALONE_SINGLE_NODE: true + DB_HOST: "postgres-db" + HTTP_PORT: "9000" + OPENAI_API_KEY: ${OPENAI_API_KEY} + +networks: + network: + driver: bridge \ No newline at end of file diff --git a/akka/demo.gif b/akka/demo.gif new file mode 100644 index 00000000..817ac87c Binary files /dev/null and b/akka/demo.gif differ diff --git a/akka/pom.xml b/akka/pom.xml new file mode 100644 index 00000000..959f123e --- /dev/null +++ b/akka/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + io.akka + akka-javasdk-parent + 3.4.6 + + + com.example + helloworld-agent + 1.0-SNAPSHOT + jar + + helloworld-agent + + diff --git a/akka/src/main/java/com/example/api/GreetingAgentEndpoint.java b/akka/src/main/java/com/example/api/GreetingAgentEndpoint.java new file mode 100644 index 00000000..98374ba5 --- /dev/null +++ b/akka/src/main/java/com/example/api/GreetingAgentEndpoint.java @@ -0,0 +1,36 @@ +package com.example.api; + +import akka.http.javadsl.model.HttpResponse; +import akka.http.javadsl.model.MediaTypes; +import akka.http.javadsl.model.headers.ContentType; +import akka.javasdk.annotations.Acl; +import akka.javasdk.annotations.http.HttpEndpoint; +import akka.javasdk.annotations.http.Post; +import akka.javasdk.http.AbstractHttpEndpoint; +import akka.javasdk.client.ComponentClient; +import akka.javasdk.http.HttpResponses; +import com.example.application.GreetingAgent; + +@HttpEndpoint("/chat") +@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL)) +public class GreetingAgentEndpoint extends AbstractHttpEndpoint { + + private final ComponentClient componentClient; + + public GreetingAgentEndpoint(ComponentClient componentClient) { + this.componentClient = componentClient; + } + + public record QueryRequest(String userId, String question) {} + + @Post("/ask") + public HttpResponse ask(QueryRequest request) { + var tokenStream = componentClient + .forAgent() + .inSession(request.userId()) + .tokenStream(GreetingAgent::ask) + .source(request.question()); + + return HttpResponses.serverSentEvents(tokenStream); + } +} \ No newline at end of file diff --git a/akka/src/main/java/com/example/api/UiEndpoint.java b/akka/src/main/java/com/example/api/UiEndpoint.java new file mode 100644 index 00000000..887d1e54 --- /dev/null +++ b/akka/src/main/java/com/example/api/UiEndpoint.java @@ -0,0 +1,34 @@ +package com.example.api; + +import akka.actor.Cancellable; +import akka.http.javadsl.model.HttpResponse; +import akka.javasdk.agent.SessionHistory; +import akka.javasdk.agent.SessionMemoryEntity; +import akka.javasdk.agent.SessionMessage; +import akka.javasdk.annotations.Acl; +import akka.javasdk.annotations.http.Get; +import akka.javasdk.annotations.http.HttpEndpoint; +import akka.javasdk.client.ComponentClient; +import akka.javasdk.http.HttpResponses; +import akka.stream.javadsl.Source; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +/** + * This Http endpoint returns the static UI page located under + * src/main/resources/static-resources/ + */ +@HttpEndpoint +@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL)) +public class UiEndpoint { + + @Get("/") + public HttpResponse index() { return HttpResponses.staticResource("index.html"); } + @Get("/style.css") + public HttpResponse style() { + return HttpResponses.staticResource("style.css"); + } +} diff --git a/akka/src/main/java/com/example/application/GreetingAgent.java b/akka/src/main/java/com/example/application/GreetingAgent.java new file mode 100644 index 00000000..af17129a --- /dev/null +++ b/akka/src/main/java/com/example/application/GreetingAgent.java @@ -0,0 +1,28 @@ +package com.example.application; + +import akka.javasdk.annotations.ComponentId; +import akka.javasdk.agent.Agent; +import akka.javasdk.agent.AgentContext; +import akka.javasdk.agent.RemoteMcpTools; + +@ComponentId("greeting-agent") +public class GreetingAgent extends Agent { + + private static final String SYSTEM_MESSAGE = + "You are a helpful assistant that teaches greetings in different languages. " + + "The user must provide a language. Always append the language used in parentheses in English " + + "(e.g., \"Bonjour (French)\"). At the end of each response, append a list of previous greetings " + + "used in the current session. " + + "For the first interaction with a user, display a greeting with their name (e.g., \"Hello !\"). " + + "The user message format is 'userId:;question:'."; + + public StreamEffect ask(String question) { + String userId = context().sessionId(); + String formattedMessage = "userId:" + userId + ";question:" + question; + + return streamEffects() + .systemMessage(SYSTEM_MESSAGE) + .userMessage(formattedMessage) + .thenReply(); + } +} \ No newline at end of file diff --git a/akka/src/main/resources/application.conf b/akka/src/main/resources/application.conf new file mode 100644 index 00000000..b0ed46b3 --- /dev/null +++ b/akka/src/main/resources/application.conf @@ -0,0 +1,15 @@ +akka.javasdk { + agent { + # Other AI models can be configured, see https://doc.akka.io/java/agents.html#model + # and https://doc.akka.io/java/model-provider-details.html for the reference configurations. + model-provider = openai + + openai { + model-name = "gpt-4o-mini" + # Environment variable override for the API key + #api-key = ${?OPENAI_API_KEY} + } + } +} + +akka.javasdk.dev-mode.enabled = true \ No newline at end of file diff --git a/akka/src/main/resources/logback.xml b/akka/src/main/resources/logback.xml new file mode 100644 index 00000000..8a8e0bb8 --- /dev/null +++ b/akka/src/main/resources/logback.xml @@ -0,0 +1,39 @@ + + + + + + + %d{HH:mm:ss.SSS} %-5level %rewriteLogger{36} - %rewriteMsg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/akka/src/main/resources/static-resources/index.html b/akka/src/main/resources/static-resources/index.html new file mode 100644 index 00000000..b8af6df6 --- /dev/null +++ b/akka/src/main/resources/static-resources/index.html @@ -0,0 +1,401 @@ + + + + + + Akka's Chatbot + + + + + + +
+
+

+ Akka's Chatbot () +

+
+
+ +
+ + + + + +
+ + +
+
+
+
+
+

Powered by Akka Agentic AI © 2025

+
+ + + \ No newline at end of file diff --git a/akka/src/main/resources/static-resources/style.css b/akka/src/main/resources/static-resources/style.css new file mode 100644 index 00000000..b11d52d7 --- /dev/null +++ b/akka/src/main/resources/static-resources/style.css @@ -0,0 +1,360 @@ +@import url('https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&display=swap'); + +body { + margin: 0; + padding: 0; + background-color: #000; + color: #fff; + font-family: "Instrument Sans", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-variation-settings: "wdth" 100; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.top-bar { + animation: gradient 6s linear infinite; + animation-direction: alternate; + background-blend-mode: hard-light; + background: linear-gradient(90deg, #ffce4a, #00dbdd); + background-size: 200% 100%; + height: 6px; + width: 100%; +} + +@keyframes gradient { + 0% { + background-position: 0; + } + to { + background-position: 100%; + } +} + +.container { + flex-grow: 1; + padding: 20px; + max-width: 1200px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +h1, h2, h3 { + color: #ffce4a; /* Secondary color for headings */ +} + +h1 { + font-size: 2.5rem; + font-weight: 700; + letter-spacing: -0.01562em; + text-align: center; + margin-bottom: 30px; +} + +h2 { + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.00833em; + margin-top: 40px; + margin-bottom: 20px; + border-bottom: 2px solid #00dbdd; /* Primary color for accent */ + padding-bottom: 10px; +} + +.form-section, .view-section { + background-color: #1a1a1a; /* Paper background */ + padding: 25px; + border-radius: 8px; + margin-bottom: 30px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +label { + display: block; + margin-bottom: 8px; + font-size: 1rem; + color: #aaa; /* Custom label color */ +} + +input[type="text"], +input[type="number"], +textarea, +select { + width: calc(100% - 24px); /* Account for padding */ + padding: 12px; + margin-bottom: 16px; + background-color: #1a1a1a; /* Input background */ + color: #fff; /* Input text color */ + border: 1px solid #aaa; /* Default border */ + border-radius: 4px; + font-size: 1rem; + font-family: "Instrument Sans", sans-serif; +} + +input[type="text"]:focus, +input[type="number"]:focus, +textarea:focus, +select:focus { + border-color: #00dbdd; /* Primary color on focus */ + outline: none; + box-shadow: 0 0 0 2px rgba(0, 219, 221, 0.3); +} + +textarea { + resize: vertical; + min-height: 100px; +} + +.button { + text-transform: uppercase; + padding: 12px 24px; + background-color: #00dbdd; /* Primary color */ + color: #000; /* Black text on primary button */ + border: 1px solid #00dbdd; + border-radius: 0; /* Sharp corners like MUI example */ + font-weight: 600; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s ease, color 0.3s ease; + display: inline-block; + text-decoration: none; + margin-right: 10px; + margin-top: 10px; +} + +.button:hover { + background-color: #000; /* Black background on hover */ + color: #00dbdd; /* Primary color text on hover */ +} + +.button-secondary { + background-color: #ffce4a; /* Secondary color */ + color: #000; + border-color: #ffce4a; +} + +.button-secondary:hover { + background-color: #000; + color: #ffce4a; + border-color: #ffce4a; +} + +.button-remove { + background-color: #4a4a4a; /* Dark grey, less prominent */ + color: #ccc; + border: 1px solid #555; + text-transform: uppercase; + padding: 10px 20px; /* Slightly smaller padding if needed */ + border-radius: 0; + font-weight: 600; + cursor: pointer; + font-size: 0.9rem; /* Slightly smaller font if needed */ + transition: background-color 0.3s ease, color 0.3s ease; + display: inline-block; + text-decoration: none; + margin-right: 10px; + margin-top: 10px; +} + +.button-remove:hover { + background-color: #555; + color: #fff; + border-color: #666; +} + +table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + margin-top: 20px; +} + +th, td { + padding: 12px 16px; + border-bottom: 4px solid #000; /* Darker border between rows */ + text-align: left; + vertical-align: top; +} + +thead th { + background-color: #1a1a1a; /* Paper background for header */ + color: #ffce4a; /* Secondary color for header text */ + font-weight: bold; + font-size: 1rem; +} + +/* Table Column Widths */ +#workOrdersTable th:nth-child(1) { width: 30%; } /* ID */ +#workOrdersTable th:nth-child(2) { width: 15%; } /* Items */ +#workOrdersTable th:nth-child(3) { width: 20%; } /* Current Step */ +#workOrdersTable th:nth-child(4) { width: 25%; } /* Errors */ +#workOrdersTable th:nth-child(5) { width: 10%; } /* Actions */ + +tbody tr:nth-of-type(even) { + /* background-color: #1a1a1a; */ /* Zebra striping if needed, but theme.js used #1a1a1a as MuiTableRow root bg */ +} + +tbody tr { + background-color: #111; /* Slightly lighter than paper for row contrast */ +} + +tbody tr:hover { + background-color: #282828; /* Hover effect for rows */ +} + +.actions a { + margin-right: 8px; + color: #00dbdd; + text-decoration: none; +} + +.actions a:hover { + color: #ffce4a; + text-decoration: underline; +} + +/* Adjusted error text color */ +.errorsCell { + color: #ff8c00; /* DarkOrange - for errors in table */ +} + +/* Modal (for updating work order) */ +.modal { + display: none; /* Hidden by default */ + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.6); /* Dimmed background */ +} + +.modal-content { + background-color: #1a1a1a; /* Paper background */ + margin: 10% auto; + padding: 30px; + border: 1px solid #333; + width: 60%; + max-width: 700px; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0,0,0,0.5); + position: relative; +} + +.close-button { + color: #aaa; + position: absolute; + top: 15px; + right: 25px; + font-size: 28px; + font-weight: bold; +} + +.close-button:hover, +.close-button:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +/* Footer */ +footer { + background-color: #1a1a1a; + color: #aaa; + text-align: center; + padding: 15px 0; + margin-top: auto; /* Pushes footer to the bottom */ + font-size: 0.875rem; +} + +footer p { + margin: 0; +} + +/* Toast Notification Styles */ +#toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 2000; /* Ensure it's above other elements like modals */ + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.toast { + background-color: #2c2c2c; /* Darker than paper, but not black */ + color: #fff; + padding: 15px 25px; + margin-bottom: 10px; + border-radius: 4px; + box-shadow: 0 3px 10px rgba(0,0,0,0.2); + opacity: 0; + transform: translateX(100%); + transition: opacity 0.5s ease, transform 0.5s ease; + border-left: 5px solid #00dbdd; /* Default to success (primary color) */ + min-width: 250px; + max-width: 400px; +} + +.toast.show { + opacity: 1; + transform: translateX(0); +} + +.toast.success { + border-left-color: #00dbdd; /* Cyan - Primary color */ +} + +.toast.error { + border-left-color: #ff8c00; /* DarkOrange - A less jarring error color */ +} + +.toast.warning { + border-left-color: #ffce4a; /* Amber - Secondary color */ +} + +.result-card { + background-color: #1a1a1a; + border-left: 4px solid #00dbdd; + padding: 20px; + margin-top: 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} + +.result-block { + margin-bottom: 20px; +} + +.result-block h3 { + color: #ffce4a; + font-size: 1.25rem; + margin-bottom: 10px; +} + +.result-block p { + color: #eee; + line-height: 1.5; + margin: 0; +} + +.result-meta { + font-size: 0.95rem; + color: #aaa; + border-top: 1px solid #333; + padding-top: 15px; +} + +.status-completed { + color: #00dbdd; + font-weight: bold; +} diff --git a/akka/src/test/java/com/example/IntegrationTest.java b/akka/src/test/java/com/example/IntegrationTest.java new file mode 100644 index 00000000..a31cdc4a --- /dev/null +++ b/akka/src/test/java/com/example/IntegrationTest.java @@ -0,0 +1,46 @@ +package com.example; + +import akka.javasdk.testkit.TestKitSupport; +import akka.javasdk.testkit.TestModelProvider; +import akka.javasdk.testkit.TestKit; +import akka.stream.javadsl.Keep; +import akka.stream.javadsl.Sink; +import com.example.application.GreetingAgent; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IntegrationTest extends TestKitSupport { + + private final TestModelProvider greetingModel = new TestModelProvider(); + + @Override + protected TestKit.Settings testKitSettings() { + return TestKit.Settings.DEFAULT + .withModelProvider(GreetingAgent.class, greetingModel); + } + + @Test + public void testGreetingAgent() throws ExecutionException, InterruptedException { + String fixedResponse = "Hola (Spanish)\n\nPrevious greetings in this session: None"; + greetingModel.fixedResponse(fixedResponse); + + var tokenStream = componentClient + .forAgent() + .inSession("test-session") + .tokenStream(GreetingAgent::ask) + .source("How do you say hello in Spanish?"); + + List tokens = tokenStream + .toMat(Sink.seq(), Keep.right()) + .run(testKit.getMaterializer()) + .toCompletableFuture() + .get(); + + String result = String.join("", tokens); + assertThat(result).isEqualTo(fixedResponse); + } +} \ No newline at end of file diff --git a/akka/src/test/resources/application.conf b/akka/src/test/resources/application.conf new file mode 100644 index 00000000..72c05e3e --- /dev/null +++ b/akka/src/test/resources/application.conf @@ -0,0 +1 @@ +akka.javasdk.agent.openai.api-key = fake_key