Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[[hibernate-assistant]]
== Hibernate Assistant
:assistant-project-dir: {root-project-dir}/hibernate-assistant

[WARNING]
====
This entire module is currently incubating and may experience breaking changes at any time, including in a micro (patch) release.
====

[[assistant-overview]]
=== Overview

The Hibernate Assistant module serves as a bridge between your existing Hibernate ORM application and generative AI services. It provides the foundational components needed to expose your domain model and database operations to Large Language Models (LLMs), enabling natural language interactions with your data layer. Rather than prescribing a specific AI provider or implementation, this module focuses on providing flexible, reusable building blocks that can be integrated with any LLM service or framework.

This module contains:

1. The `HibernateAssistant` interface: to provide a simple, provider-agnostic, natural-language focused API to Hibernate ORM's persistence capabilities.
2. Serialization utilities: to ease the use of Hibernate ORM in the context of generative AI, for example when implementing the above.

No implementation is included, but the above provides the building blocks for integration with generative AI services/APIs.

[[assistant-gen-ai]]
==== Generative AI integration considerations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect heading level

Suggested change
==== Generative AI integration considerations
=== Generative AI integration considerations


Hibernate ORM comes with several advantages when interfacing with an LLM and accessing underlying RDBMS data, mainly:

Access to data is *constrained to the mapped domain model*::
The only tables the LLM will be able to access are the ones that have a corresponding entity class, and only columns listed as fields in your objects can be read. Custom filters and SQL restrictions can be applied to further restrict the scope of the data exposed through these tools. You don’t have to worry about creating custom database-level users or permissions only to ensure sensitive information is not exposed to AI services;

Easy results consumption::
Natively maps results to *Java objects* for direct application consumption, but can also be serialized and passed back to the model to obtain an *informed natural language response based on your existing data*;

Type-safety and query validation::
Hibernate’s query language parsing can identify the *type of query* being executed and prevent accidental data modifications when the user only meant to read data;

*Fail-early* in case the generated statements are incorrect::
Thanks to Hibernate’s advanced query validation and type-safety features, we don’t need to make a round-trip to the database before noticing a problem, increasing both reliability and overall performance. It’s also easy to understand what the problem with the generated query is thanks to clear error messages, and attempt to solve it either manually or with subsequent prompts;

Bridge the gap with natural language::
With HQL it’s easier to write more *complex queries* involving multiple entities (i.e. tables) thanks to associations, embeddable values and inheritance. LLMs have an easier time generating valid queries that provide useful information to the user when compared to plain SQL, since Hibernate's query language is closer to natural language.


[[assistant-serialization]]
==== Serialization Components
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect heading level

Suggested change
==== Serialization Components
=== Serialization Components


To facilitate communication between Hibernate and LLM providers, the module includes two key Service Provider Interfaces (SPIs):

`MetamodelSerializer`:: Generates a structured textual representation of your Hibernate mapping model, including entity classes, relationships, properties, and constraints. This allows the LLM to understand your domain model's structure and semantics.

`ResultsSerializer`:: Converts query results and data into a structured textual format suitable for LLM consumption and interpretation. This enables the AI to reason about actual data from your database.

Default JSON-based implementations of both serializers are provided, offering a ready-to-use foundation for most integration scenarios.
1 change: 1 addition & 0 deletions documentation/src/main/asciidoc/userguide/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ include::chapters/beans/Beans.adoc[]
include::chapters/portability/Portability.adoc[]
include::chapters/statistics/Statistics.adoc[]
include::chapters/tooling/Tooling.adoc[]
include::chapters/assistant/Assistant.adoc[]
include::appendices/BestPractices.adoc[]

include::Credits.adoc[]
Expand Down
18 changes: 18 additions & 0 deletions hibernate-assistant/hibernate-assistant.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/

plugins {
id "local.publishing-java-module"
id "local.publishing-group-relocation"
}

description = 'Tools to integrate Hibernate with LLMs and generative AI functionalities.'

dependencies {
api project( ':hibernate-core' )

testImplementation project( ':hibernate-testing' )
testImplementation libs.jackson
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.tool.language;

import org.hibernate.Incubating;
import org.hibernate.SharedSessionContract;
import org.hibernate.query.SelectionQuery;

/**
* Hibernate Assistant allows interacting with an underlying LLM to help you retrieve persistent data.
* It leverages Hibernate ORM's mapping models, query language, cross-platform support and
* built-in data restrictions to make access to information stored in relational databases
* as easy as a natural language prompt.
*/
@Incubating
public interface HibernateAssistant {
/**
* Creates a {@link SelectionQuery} by providing the specified natural language {@code message} to the LLM
* and interpreting the obtained response.
*
* @param message the natural language prompt
* @param session Hibernate session
*
* @return the {@link SelectionQuery} generated by the LLM
*/
default SelectionQuery<?> createAiQuery(String message, SharedSessionContract session) {
return createAiQuery( message, session, null );
}

/**
* Creates a {@link SelectionQuery} by providing the specified natural language {@code message} to the LLM
* and interpreting the obtained response.
*
* @param message the natural language prompt
* @param session Hibernate session
* @param resultType The {@link Class} representing the expected query result type
*
* @return the {@link SelectionQuery} generated by the LLM
*/
<T> SelectionQuery<T> createAiQuery(String message, SharedSessionContract session, Class<T> resultType);

/**
* Prompts the underlying LLM with the provided natural language message and tries to answer it with
* data extracted from the database through the persistence model.
*
* @param message the natural language request
* @param session Hibernate session
*
* @return a natural language response based on the results of the query
*/
String executeQuery(String message, SharedSessionContract session);

/**
* Executes the given {@link SelectionQuery}, and provides a natural language
* response by passing the resulting data back to the underlying LLM.
* <p>
* To directly obtain a natural language response from a natural language prompt,
* you can use {@link #executeQuery(String, SharedSessionContract)} instead.
* <p>
* If you wish to execute the query manually and obtain the structured results yourself,
* you should use {@link SelectionQuery}'s direct execution methods, e.g. {@link SelectionQuery#getResultList()}
* or {@link SelectionQuery#getSingleResult()}.
*
* @param query the AI query to execute
* @param session the session in which to execute the query
*
* @return a natural language response based on the results of the query
*/
String executeQuery(SelectionQuery<?> query, SharedSessionContract session);

/**
* Reset the assistant's current chat context. This can be helpful when
* creating a new {@link SelectionQuery} that should not rely on the context
* of previous requests.
*/
void clear();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.tool.language.internal;

import org.hibernate.metamodel.model.domain.ManagedDomainType;
import org.hibernate.tool.language.spi.MetamodelSerializer;
import org.hibernate.type.format.StringJsonDocumentWriter;

import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.EmbeddableType;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.IdentifiableType;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.MappedSuperclassType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Implementation of {@link MetamodelSerializer} that represents the {@link Metamodel} as a JSON array of mapped objects.
*/
public class MetamodelJsonSerializerImpl implements MetamodelSerializer {
public static MetamodelJsonSerializerImpl INSTANCE = new MetamodelJsonSerializerImpl();

/**
* Utility method that generates a JSON string representation of the mapping information
* contained in the provided {@link Metamodel metamodel} instance. The representation
* does not follow a strict scheme, and is more akin to natural language, as it's
* mainly meant for consumption by a LLM.
*
* @param metamodel the metamodel instance containing information on the persistence structures
*
* @return the JSON representation of the provided {@link Metamodel metamodel}
*/
@Override
public String toString(Metamodel metamodel) {
final List<Map<String, Object>> entities = new ArrayList<>();
final List<Map<String, Object>> embeddables = new ArrayList<>();
final List<Map<String, Object>> mappedSupers = new ArrayList<>();
for ( ManagedType<?> managedType : metamodel.getManagedTypes() ) {
switch ( managedType.getPersistenceType() ) {
case ENTITY -> entities.add( getEntityTypeDescription( (EntityType<?>) managedType ) );
case EMBEDDABLE -> embeddables.add( getEmbeddableTypeDescription( (EmbeddableType<?>) managedType ) );
case MAPPED_SUPERCLASS -> mappedSupers.add( getMappedSuperclassTypeDescription( (MappedSuperclassType<?>) managedType ) );
default ->
throw new IllegalStateException( "Unexpected persistence type for managed type [" + managedType + "]" );
}
}
return toJson( Map.of(
"entities", entities,
"mappedSuperclasses", mappedSupers,
"embeddables", embeddables
) );
}

private static String toJson(Map<String, Object> map) {
if ( map.isEmpty() ) {
return "{}";
}

final StringJsonDocumentWriter writer = new StringJsonDocumentWriter( new StringBuilder() );
toJson( map, writer );
return writer.toString();
}

private static void toJson(Object value, StringJsonDocumentWriter writer) {
if ( value instanceof String strValue ) {
writer.stringValue( strValue );
}
else if ( value instanceof Boolean boolValue ) {
writer.booleanValue( boolValue );
}
else if ( value instanceof Number numValue ) {
writer.numericValue( numValue );
}
else if ( value instanceof Map<?, ?> map ) {
writer.startObject();
for ( final var entry : map.entrySet() ) {
writer.objectKey( entry.getKey().toString() );
toJson( entry.getValue(), writer );
}
writer.endObject();
}
else if ( value instanceof Collection<?> collection ) {
writer.startArray();
for ( final var item : collection ) {
toJson( item, writer );
}
writer.endArray();
}
else if ( value == null ) {
writer.nullValue();
}
else {
throw new IllegalArgumentException( "Unsupported value type: " + value.getClass().getName() );
}
}

private static void putIfNotNull(Map<String, Object> map, String key, Object value) {
if ( value != null ) {
map.put( key, value );
}
}

private static <T> Map<String, Object> getEntityTypeDescription(EntityType<T> entityType) {
final Map<String, Object> map = new HashMap<>( 5 );
map.put( "name", entityType.getName() );
map.put( "class", entityType.getJavaType().getTypeName() );
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) entityType ) );
putIfNotNull( map, "identifierAttribute", identifierDescriptor( entityType ) );
map.put( "attributes", attributeArray( entityType.getAttributes() ) );
return map;
}

private static String superTypeDescriptor(ManagedDomainType<?> managedType) {
final var superType = managedType.getSuperType();
return superType != null ? superType.getJavaType().getTypeName() : null;
}

private static <T> Map<String, Object> getMappedSuperclassTypeDescription(MappedSuperclassType<T> mappedSuperclass) {
final Class<T> javaType = mappedSuperclass.getJavaType();
final Map<String, Object> map = new HashMap<>( 5 );
map.put( "name", javaType.getSimpleName() );
map.put( "class", javaType.getTypeName() );
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) mappedSuperclass ) );
putIfNotNull( map, "identifierAttribute", identifierDescriptor( mappedSuperclass ) );
map.put( "attributes", attributeArray( mappedSuperclass.getAttributes() ) );
return map;
}

private static <T> String identifierDescriptor(IdentifiableType<T> identifiableType) {
final Type<?> idType = identifiableType.getIdType();
if ( idType != null ) {
final var id = identifiableType.getId( idType.getJavaType() );
return id.getName();
}
else {
return null;
}
}

private static <T> Map<String, Object> getEmbeddableTypeDescription(EmbeddableType<T> embeddableType) {
final Class<T> javaType = embeddableType.getJavaType();
final Map<String, Object> map = new HashMap<>( 4 );
map.put( "name", javaType.getSimpleName() );
map.put( "class", javaType.getTypeName() );
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) embeddableType ) );
map.put( "attributes", attributeArray( embeddableType.getAttributes() ) );
return map;
}

private static <T> List<Map<String, String>> attributeArray(Set<Attribute<? super T, ?>> attributes) {
if ( attributes.isEmpty() ) {
return List.of();
}

return attributes.stream().map( attribute -> {
final String name = attribute.getName();
String type = attribute.getJavaType().getTypeName();
// add key and element types for plural attributes
if ( attribute instanceof PluralAttribute<?, ?, ?> pluralAttribute ) {
type += "<";
final var collectionType = pluralAttribute.getCollectionType();
if ( collectionType == PluralAttribute.CollectionType.MAP ) {
type += ( (MapAttribute<?, ?, ?>) pluralAttribute ).getKeyJavaType().getTypeName() + ",";
}
type += pluralAttribute.getElementType().getJavaType().getTypeName() + ">";
}
return Map.of(
"type", type,
"name", name
);
} ).toList();
}
}
Loading