Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions src/main/java/io/github/jeddict/ai/lang/JeddictBrain.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,8 @@
import io.github.jeddict.ai.agent.pair.HackerWithoutTools;
import io.github.jeddict.ai.agent.pair.PairProgrammer;
import static io.github.jeddict.ai.lang.InteractionMode.INTERACTIVE;
import io.github.jeddict.ai.util.ClassLoaderUtil;
import io.github.jeddict.ai.util.PropertyChangeEmitter;
import java.lang.reflect.InvocationTargetException;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -210,7 +207,6 @@ public JeddictBrain withMemory(int size) {
* @return an instance of the configured agent
*/
public <T> T pairProgrammer(PairProgrammer.Specialist specialist) {
ClassLoaderUtil.usePluginClassLoaderIfNeeded(getClass());
ChatModel chatModel = null;

if (specialist == PairProgrammer.Specialist.HACKER) {
Expand Down Expand Up @@ -337,23 +333,6 @@ protected boolean probeToolSupport() {
);

return toolsSupport;
} catch (final NoClassDefFoundError | java.util.ServiceConfigurationError e) {

LOG.severe(()
-> "Error probing tool support (likely classloader conflict), returning false %s\n%s".formatted(
e.toString(),
Arrays.toString(e.getStackTrace())
)
);

ClassLoaderUtil.enablePluginClassLoaderFallback();

final NotifyDescriptor nd = new NotifyDescriptor.Message(
"AI tool support probe failed due to a classpath conflict: " + e.toString(),
NotifyDescriptor.ERROR_MESSAGE
);
DialogDisplayer.getDefault().notify(nd);

} catch (final Throwable t) {
LOG.severe(() ->
"error probing tool support, returning false %s\n%s".formatted(
Expand Down
211 changes: 211 additions & 0 deletions src/main/java/io/github/jeddict/ai/lang/JeddictJsonCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright 2026 the original author or authors from the LLMTooliy project
* (https://stefanofornari.github.io/llm-toolify).
*
* 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.
*/
package io.github.jeddict.ai.lang;

import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import dev.langchain4j.Internal;
import dev.langchain4j.internal.Json;

import java.io.IOException;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Optional;

/**
* A JSON codec implementation using Jackson for serialization and deserialization.
* Provides methods to convert objects to their JSON representation and parse JSON strings into objects.
* Customizes the behavior of the Jackson {@link ObjectMapper} to support serialization and deserialization
* of Java 8 date/time types such as {@link LocalDate}, {@link LocalTime}, and {@link LocalDateTime}.
*
* This is class has been taken directly from LangChain4j to solve a classpath
* conflict in using teh library langchain4j in a NB module.
*
* References:
* - https://github.com/jeddict/jeddict-ai/issues/298
* - https://github.com/langchain4j/langchain4j/issues/4726
* - https://docs.langchain4j.dev/tutorials/json/
*/
@Internal
class JeddictJsonCodec implements Json.JsonCodec {

private final ObjectMapper objectMapper;

private static ObjectMapper createObjectMapper() {

SimpleModule module = new SimpleModule("langchain4j-module");

module.addSerializer(LocalDate.class, new StdSerializer<>(LocalDate.class) {
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.format(ISO_LOCAL_DATE));
}
});

module.addDeserializer(LocalDate.class, new JsonDeserializer<>() {
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (node.isObject()) {
int year = node.get("year").asInt();
int month = node.get("month").asInt();
int day = node.get("day").asInt();
return LocalDate.of(year, month, day);
} else {
return LocalDate.parse(node.asText(), ISO_LOCAL_DATE);
}
}
});

module.addSerializer(LocalTime.class, new StdSerializer<>(LocalTime.class) {
@Override
public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.format(ISO_LOCAL_TIME));
}
});

module.addDeserializer(LocalTime.class, new JsonDeserializer<>() {
@Override
public LocalTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (node.isObject()) {
int hour = node.get("hour").asInt();
int minute = node.get("minute").asInt();
int second = Optional.ofNullable(node.get("second")).map(JsonNode::asInt).orElse(0);
int nano = Optional.ofNullable(node.get("nano")).map(JsonNode::asInt).orElse(0);
return LocalTime.of(hour, minute, second, nano);
} else {
return LocalTime.parse(node.asText(), ISO_LOCAL_TIME);
}
}
});

module.addSerializer(LocalDateTime.class, new StdSerializer<>(LocalDateTime.class) {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value.format(ISO_LOCAL_DATE_TIME));
}
});

module.addDeserializer(LocalDateTime.class, new JsonDeserializer<>() {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (node.isObject()) {
JsonNode date = node.get("date");
int year = date.get("year").asInt();
int month = date.get("month").asInt();
int day = date.get("day").asInt();
JsonNode time = node.get("time");
int hour = time.get("hour").asInt();
int minute = time.get("minute").asInt();
int second = Optional.ofNullable(time.get("second")).map(JsonNode::asInt).orElse(0);
int nano = Optional.ofNullable(time.get("nano")).map(JsonNode::asInt).orElse(0);
return LocalDateTime.of(year, month, day, hour, minute, second, nano);
} else {
return LocalDateTime.parse(node.asText(), ISO_LOCAL_DATE_TIME);
}
}
});

// FAIL_ON_UNKNOWN_PROPERTIES is enabled by default
// to prevent issues caused by LLM hallucinations
return JsonMapper.builder()
.visibility(FIELD, ANY)
.enable(INDENT_OUTPUT)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build()
.findAndRegisterModules()
.registerModule(module);
}

/**
* Constructs a JacksonJsonCodec instance with the provided ObjectMapper.
*
* @param objectMapper the ObjectMapper to use for JSON serialization and deserialization.
*/
public JeddictJsonCodec(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

/**
* Constructs a JacksonJsonCodec instance with a default ObjectMapper.
* The default ObjectMapper is configured with custom serializers and deserializers
* for Java 8 date/time types such as LocalDate, LocalTime, and LocalDateTime.
* It also registers other modules found on the classpath, enables formatted JSON output,
* and throws exceptions for unknown properties to improve handling of unexpected input.
*/
public JeddictJsonCodec() {
this(createObjectMapper());
}

@Override
public String toJson(Object o) {
try {
return objectMapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

@Override
public <T> T fromJson(String json, Class<T> type) {
try {
return objectMapper.readValue(json, type);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

@Override
public <T> T fromJson(String json, Type type) {
try {
return objectMapper.readValue(json, objectMapper.constructType(type));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

/**
* Returns the ObjectMapper instance used for JSON processing.
*
* @return the ObjectMapper instance.
*/
public ObjectMapper getObjectMapper() {
return objectMapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2026 the original author or authors from the LLMTooliy project
* (https://stefanofornari.github.io/llm-toolify).
*
* 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.
*/

package io.github.jeddict.ai.lang;


import dev.langchain4j.internal.Json;


import dev.langchain4j.spi.json.JsonCodecFactory;
import java.util.logging.Logger;

public class JeddictJsonCodecFactory implements JsonCodecFactory {

private final Logger LOG = Logger.getLogger(getClass().getName());

@Override
public Json.JsonCodec create() {
final JeddictJsonCodec codec = new JeddictJsonCodec();

LOG.finest(() -> "using json codec " + codec);

return codec;
}


}
38 changes: 0 additions & 38 deletions src/main/java/io/github/jeddict/ai/util/ClassLoaderUtil.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.github.jeddict.ai.lang.JeddictJsonCodecFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 the original author or authors from the LLMTooliy project
* (https://stefanofornari.github.io/llm-toolify).
*
* 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.
*/

package io.github.jeddict.ai.lang;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.BDDAssertions.then;
import java.util.ServiceLoader;
import dev.langchain4j.spi.json.JsonCodecFactory;
import io.github.jeddict.ai.test.TestBase;
import java.util.logging.Level;

/**
*
*/
public class JeddictJsonCodecFactoryTest extends TestBase {

@Test
public void json_codec_factory_service_loading() {
ServiceLoader<JsonCodecFactory> loader = ServiceLoader.load(JsonCodecFactory.class);
then(loader.iterator().hasNext()).isTrue();

JsonCodecFactory factory = loader.iterator().next();
then(factory).isNotNull();
then(factory).isInstanceOf(JeddictJsonCodecFactory.class);
then(factory).isInstanceOf(JsonCodecFactory.class);
}

@Test
public void lanchain4j_picks_jeddict_factory() {
dev.langchain4j.internal.Json.toJson(1);
then(logHandler.getMessages(Level.FINEST)).hasSize(1)
.element(0).asString().startsWith("using json codec " + JeddictJsonCodec.class.getName());
}
}
Loading