Skip to content

Commit

Permalink
Small improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Luca Venturi committed Jan 26, 2025
1 parent 9005fe9 commit f0543fa
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Fibry 2.X is a Distributed Actor System, meaning that it can use multiple machin
Fibry provides a simple, generic, support to contact (named) actors running on other machines. It is based on these principles: channels (the communication method) and serializers / deserializers (to transmit the message via network).

Remote actors can be created using **newRemoteActor()**, **newRemoteActorWithReturn()** and **newRemoteActorSendOnly()**, from ActorSystem.
FOr more details, please check the examples in the **eu.lucaventuri.examples.distributed** package.
For more details, please check the examples in the **eu.lucaventuri.examples.distributed** package.

It provides some interfaces:
- **RemoteActorChannel**: an interface to send messages to named actors running on remote machines; these actors can return a value.
Expand Down
28 changes: 14 additions & 14 deletions src/main/java/eu/lucaventuri/common/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,41 @@
import java.util.concurrent.atomic.AtomicReference;

public class JsonUtils {
private static AtomicReference<JsonProcessor> serializer = new AtomicReference<>();
private static AtomicReference<JsonProcessor> processor = new AtomicReference<>();

public static void setSerializer(JsonProcessor serializer) {
JsonUtils.serializer.set(serializer);
public static void setProcessor(JsonProcessor processor) {
JsonUtils.processor.set(processor);
}

public static String toJson(Object obj) {
return ensureSerializer().toJson(obj);
return ensureProcessor().toJson(obj);
}

public static String traverseAsString(String json, Object... paths) {
return ensureSerializer().traverseAsString(json, paths);
return ensureProcessor().traverseAsString(json, paths);
}

private static JsonProcessor ensureSerializer() {
var ser = serializer.get();
private static JsonProcessor ensureProcessor() {
var proc = processor.get();

if (ser == null) {
if (proc == null) {
synchronized(JsonUtils.class) {
ser = serializer.get();
proc = processor.get();

if (ser == null) {
if (proc == null) {
if (JacksonProcessor.isJacksonAvailable()) {
ser = new JacksonProcessor();
proc = new JacksonProcessor();
}
else {
ser = new JsonMiniProcessor();
proc = new JsonMiniProcessor();
System.err.println("Please call setSerializer() to use a real production JSON serializer. For now, a simple one useful for experiments will be used instead. ");
}

setSerializer(ser);
setProcessor(proc);
}
}
}

return ser;
return proc;
}
}
16 changes: 15 additions & 1 deletion src/main/java/eu/lucaventuri/common/RecordUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ public static <T extends Record> String replaceAllFields(String input, T record)
return input;
}

/**
* Replaces a specific placeholder in the given input string with the value of the corresponding field
* from the provided record. The placeholder in the input string must match the field name and be
* enclosed in curly braces, e.g., {fieldName}.
*
* @param <T> the type of the record, which must extend {@link Record}
* @param input the input string containing a placeholder to be replaced
* @param record the record whose field value will be used for replacement
* @param attributeName the name of the record's field to replace in the input string
* @return the input string with the specified placeholder replaced by the corresponding field value from the record,
* or the input string unchanged if the attribute name does not match any field in the record
* @throws IllegalArgumentException if the record, input string, or attribute name is null
* @throws RuntimeException if an error occurs while accessing the field value of the record
*/
public static <T extends Record> String replaceField(String input, T record, String attributeName) {
if (record == null || input == null || attributeName == null) {
throw new IllegalArgumentException("Record, input string, and attribute name cannot be null");
Expand All @@ -215,7 +229,7 @@ public static <T extends Record> String replaceField(String input, T record, Str
return input;
}

public static <T extends Record> String replaceFields(T record, String input, Collection<String> attributeNames) {
public static <T extends Record> String replaceFields(String input, T record, String... attributeNames) {
if (record == null || input == null || attributeNames == null) {
throw new IllegalArgumentException("Record, input string, and attribute names cannot be null");
}
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/eu/lucaventuri/fibry/ai/AiAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public record AgentExecutionRequest<S extends Enum, I>(I input, BiConsumer<S, I>
record States<S>(S prevState, S curState) {
}

public AiAgent(FsmTemplateActor<S, S, AgentState<S, I>, MessageOnlyActor<FsmContext<S, S, AgentState<S, I>>, AgentState<S, I>, Void>, AgentState<S, I>> fsm, S initialState, S finalState, Map<S, List<S>> defaultStates, boolean parallelStatesProcessing, boolean skipLastStates) {
super(new FibryQueue<>(), null, null, Integer.MAX_VALUE);
public AiAgent(FsmTemplateActor<S, S, AgentState<S, I>, MessageOnlyActor<FsmContext<S, S, AgentState<S, I>>, AgentState<S, I>, Void>, AgentState<S, I>> fsm, S initialState, S finalState, Map<S, List<S>> defaultStates, String actorName, int capacity, boolean parallelStatesProcessing, boolean skipLastStates) {
super(ActorSystem.getOrCreateActorQueue(actorName, capacity), null, null, Integer.MAX_VALUE);
this.fsm = fsm;
this.initialState = initialState;
this.finalState = finalState;
Expand Down Expand Up @@ -132,4 +132,8 @@ public I process(I input, int timeout, TimeUnit timeUnit, BiConsumer<S, I> state
public I process(I input, BiConsumer<S, I> stateListener) {
return process(input, 1, TimeUnit.HOURS, stateListener);
}

public static <S extends Enum, I extends Record> AiAgentBuilderActor<S, I> builder(boolean autoGuards) {
return new AiAgentBuilderActor<>(autoGuards);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ private void goToAll(FsmBuilderActor<S, S, AgentState<S, I>, MessageOnlyActor<Fs
}

public AiAgent<S, I> build(S initialState, S finalState, boolean parallelStatesProcessing) {
return build(initialState, finalState, parallelStatesProcessing, null, Integer.MAX_VALUE);
}

public AiAgent<S, I> build(S initialState, S finalState, boolean parallelStatesProcessing, String actorName, int queueCapacity) {
if (initialState == null)
throw new IllegalArgumentException("The initial state cannot be null!");

Expand All @@ -144,7 +148,7 @@ public AiAgent<S, I> build(S initialState, S finalState, boolean parallelStatesP
}
);

return new AiAgent<>(builder.build(), initialState, finalState, defaultStates, parallelStatesProcessing, autoGuards);
return new AiAgent<>(builder.build(), initialState, finalState, defaultStates, actorName, queueCapacity, parallelStatesProcessing, autoGuards);
}

Function<FsmContext<S, S, AgentState<S, I>>, AgentState<S, I>> logicWithGuard(S state, Function<FsmContext<S, S, AgentState<S, I>>, AgentState<S, I>> actorLogic, GuardLogic<S, I> guard) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ChatGpt {
public class ChatGPT {
public static final LLM GPT_MODEL_4O = llm("gpt-4o");
public static final LLM GPT_MODEL_4O_MINI = llm("gpt-4o-mini");
public static final LLM GPT_MODEL_O1 = llm("o1");
Expand Down Expand Up @@ -104,7 +103,7 @@ public static LLM llm(String modelName) {
return new LLM() {
@Override
public String call(List<LlmMessage> promptParts) {
return ChatGpt.request(modelName, promptParts);
return ChatGPT.request(modelName, promptParts);
}

@Override
Expand Down
50 changes: 24 additions & 26 deletions src/test/java/eu/lucaventuri/examples/AiAgentExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,45 @@
import eu.lucaventuri.fibry.ai.*;
import static eu.lucaventuri.common.RecordUtils.replaceAllFields;
import static eu.lucaventuri.common.RecordUtils.replaceField;
import static eu.lucaventuri.common.RecordUtils.replaceFields;

import java.util.List;
import java.util.concurrent.TimeUnit;

public class AiAgentExample {
public static class AiAgentVacations {
private static final String promptFood = "You are an foodie from {country}. Please tell me the top 10 cities for food in {country}.";
private static final String promptDance = "You are an dancer from {country}. Please tell me the top 10 cities in {country} where I can dance Salsa and Bachata.";
private static final String promptFood = "You are a foodie from {country}. Please tell me the top 10 cities for food in {country}.";
private static final String promptActivity = "You are from {country}, and know it inside out. Please tell me the top 10 cities in {country} where I can {goal}";
private static final String promptSea = "You are an expert traveler, and you {country} inside out. Please tell me the top 10 cities for sea vacations in {country}.";
private static final String promptChoice = """
You enjoy traveling, dancing Salsa and Bachata, eating good food and staying at the sea. Please analyze the following suggestions from your friends for a vacation in {country} and choose the best city to visit, offering the best mix of Salsa and Bachata dancing, food and sea.
You enjoy traveling, eating good food and staying at the sea, but you also want to {activity}. Please analyze the following suggestions from your friends for a vacation in {country} and choose the best city to visit, offering the best mix of food and sea and where you can {activity}.
Food suggestions: {food}.
Dance suggestions: {dance}.
Activity suggestions: {activity}.
Sea suggestions: {sea}.
""";

enum VacationStates {
CITIES, CHOOSE
CITIES, CHOICE
}
public record VacationContext(String country, String food, String dance, String sea, String proposal) {
public static VacationContext from(String country) {
return new VacationContext(country, null, null, null, null);
public record VacationContext(String country, String goal, String food, String activity, String sea, String proposal) {
public static VacationContext from(String country, String goal) {
return new VacationContext(country, goal, null, null, null, null);
}
}

public static AiAgent<?, VacationContext> buildAgent(LLM modelSearch, LLM modelThink) {
var builder = new AiAgentBuilderActor<VacationStates, VacationContext>(false);
AgentNode<VacationStates, VacationContext> nodeFood = state -> state.setAttribute("food", modelSearch.call("user", replaceField(promptFood, state.data(), "country")));
AgentNode<VacationStates, VacationContext> nodeDance = state -> state.setAttribute("dance", modelSearch.call("user", replaceField(promptDance, state.data(), "country")));
AgentNode<VacationStates, VacationContext> nodeActivity = state -> state.setAttribute("activity", modelSearch.call("user", replaceFields(promptActivity, state.data(), "country", "goal")));
AgentNode<VacationStates, VacationContext> nodeSea = state -> state.setAttribute("sea", modelSearch.call("user", replaceField(promptSea, state.data(), "country")));
AgentNode<VacationStates, VacationContext> nodeChoice = state -> {
var prompt = replaceAllFields(promptChoice, state.data());
System.out.println("***** CHOICE PROMPT: " + prompt);
return state.setAttribute("proposal", modelThink.call("user", prompt));
};

builder.addStateParallel(VacationStates.CITIES, VacationStates.CHOOSE, 1, List.of(nodeFood, nodeDance, nodeSea), null);
builder.addState(VacationStates.CHOOSE, null, 1, nodeChoice, null);
var builder = AiAgent.<VacationStates, VacationContext>builder(true);
builder.addStateParallel(VacationStates.CITIES, VacationStates.CHOICE, 1, List.of(nodeFood, nodeActivity, nodeSea), null);
builder.addState(VacationStates.CHOICE, null, 1, nodeChoice, null);

return builder.build(VacationStates.CITIES, null, false);
}
Expand All @@ -49,20 +50,17 @@ public static AiAgent<?, VacationContext> buildAgent(LLM modelSearch, LLM modelT
public static class AiAgentTravelAgency {
private static final String promptDestination = "Read the following text describing a destination for a vacation and extract the destination as a simple city and country, no preamble. Just the city and the country. {proposal}";
private static final String promptCost = "You are an expert travel agent. A customer asked you to estimate the cost of travelling from {startCity}, {startCountry} to {destination}, for {adults} adults and {kids} kids}";

enum TravelStates {
SEARCH, CALCULATE
}

public record TravelContext(String startCity, String startCountry, String destination, int adults, int kids, String cost, String proposal) {
public static TravelContext from(String startCity, String startCountry, int adults, int kids) {
return new TravelContext(startCity, startCountry, null, adults, kids, null, null);
}
}
public record TravelContext(String startCity, String startCountry, int adults, int kids, String destination, String cost, String proposal) { }

public static AiAgent<?, TravelContext> buildAgent(LLM model, AiAgent<?, AiAgentVacations.VacationContext> vacationsAgent, String country) {
var builder = new AiAgentBuilderActor<TravelStates, TravelContext>(false);
public static AiAgent<?, TravelContext> buildAgent(LLM model, AiAgent<?, AiAgentVacations.VacationContext> vacationsAgent, String country, String goal, boolean debugSubAgentStates) {
var builder = AiAgent.<TravelStates, TravelContext>builder(false);
AgentNode<TravelStates, TravelContext> nodeSearch = state -> {
var vacationProposal = vacationsAgent.process(AiAgentVacations.VacationContext.from(country), 1, TimeUnit.MINUTES);
var vacationProposal = vacationsAgent.process(AiAgentVacations.VacationContext.from(country, goal), 1, TimeUnit.MINUTES, (st, info) -> System.out.print(debugSubAgentStates ? st + ": " + info : ""));
return state.setAttribute("proposal", vacationProposal.proposal())
.setAttribute("destination", model.call(promptDestination.replaceAll("\\{proposal\\}", vacationProposal.proposal())));
};
Expand All @@ -77,14 +75,14 @@ public static AiAgent<?, TravelContext> buildAgent(LLM model, AiAgent<?, AiAgent


public static void main(String[] args) {
try (var vacationsAgent = AiAgentVacations.buildAgent(ChatGpt.GPT_MODEL_4O, ChatGpt.GPT_MODEL_O1_MINI)) {
try (var travelAgent = AiAgentTravelAgency.buildAgent(ChatGpt.GPT_MODEL_4O, vacationsAgent, "Italy")) {
var result = travelAgent.process(AiAgentTravelAgency.TravelContext.from("Oslo", "Norway", 2,2), (state, info) -> System.out.println(state + ": " + info));
try (var vacationsAgent = AiAgentVacations.buildAgent(ChatGPT.GPT_MODEL_4O, ChatGPT.GPT_MODEL_O1_MINI)) {
try (var travelAgent = AiAgentTravelAgency.buildAgent(ChatGPT.GPT_MODEL_4O, vacationsAgent, "Italy", "Dance Salsa and Bachata", true)) {
var result = travelAgent.process(new AiAgentTravelAgency.TravelContext("Oslo", "Norway", 2, 2, null, null, null), (state, info) -> System.out.println(state + ": " + info));

System.out.println("\n\n\n***** FINAL ANALYSIS *****\n\n\n");
System.out.println(result.proposal);
System.out.println(result.destination);
System.out.println(result.cost());
System.out.println("\n\n\n*** Destination: " + result.destination());
System.out.println("*** Proposal: " + result.proposal());
System.out.println("\n\n\n*** Cost: " + result.cost());
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/test/java/eu/lucaventuri/fibry/TestAIAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import eu.lucaventuri.common.SystemUtils;
import eu.lucaventuri.fibry.ai.AgentNode;
import eu.lucaventuri.fibry.ai.AiAgent;
import eu.lucaventuri.fibry.ai.AiAgentBuilderActor;
import org.junit.Assert;
import org.junit.Test;

Expand Down Expand Up @@ -50,7 +49,7 @@ public record ShoppingContext(int priceVeggies, int priceMeat, int totalPaid, bo

private AiAgent<TravelState, TravelInfo> buildVacationAgent(boolean parallel, int parallelism, int sleep) {
long start = System.currentTimeMillis();
var builder = new AiAgentBuilderActor<TravelState, TravelInfo>(false);
var builder = AiAgent.<TravelState, TravelInfo>builder(false);
List<AgentNode<TravelState, TravelInfo>> nodes = List.of(
state -> {
System.out.println("Italy starts at " + (System.currentTimeMillis() - start));
Expand Down Expand Up @@ -83,7 +82,7 @@ private AiAgent<TravelState, TravelInfo> buildVacationAgent(boolean parallel, in

private AiAgent<WaitingState, WaitingInfo> buildWaitingAgent(boolean parallelStateProcessing, int sleep) {
long start = System.currentTimeMillis();
var builder = new AiAgentBuilderActor<WaitingState, WaitingInfo>(true);
var builder = AiAgent.<WaitingState, WaitingInfo>builder(true);
builder.addStateMulti(WaitingState.A, List.of(WaitingState.W1, WaitingState.W2, WaitingState.W3), 0, state -> state, null);
builder.addState(WaitingState.W1, WaitingState.DONE, 0, state -> {
SystemUtils.sleep(sleep);
Expand Down Expand Up @@ -223,7 +222,7 @@ private static void asyncExec(AiAgent<TravelState, TravelInfo> aiAgent) throws I
}

private AiAgent<ShoppingState, ShoppingContext> buildShoppingAgent() {
var builder = new AiAgentBuilderActor<ShoppingState, ShoppingContext>(true);
var builder = AiAgent.<ShoppingState, ShoppingContext>builder(true);
builder.addStateSerial(ShoppingState.COLLECT_FOOD, List.of(ShoppingState.PAY, ShoppingState.LOOK_AROUND), 1, List.of(
state -> state.setAttribute("priceVeggies", 100),
state -> state.setAttribute("priceMeat", 200) ), null);
Expand Down

0 comments on commit f0543fa

Please sign in to comment.