Skip to content

Commit 9ddcf11

Browse files
committed
HHH-19880 New assistant module (moved from hibernate-tools/language)
1 parent 7cc8af7 commit 9ddcf11

File tree

16 files changed

+1548
-0
lines changed

16 files changed

+1548
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[[hibernate-assistant]]
2+
== Hibernate Assistant
3+
:assistant-project-dir: {root-project-dir}/hibernate-assistant
4+
5+
[WARNING]
6+
====
7+
This entire module is currently incubating and may experience breaking changes at any time, including in a micro (patch) release.
8+
====
9+
10+
[[assistant-overview]]
11+
=== Overview
12+
13+
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.
14+
15+
This module contains:
16+
17+
1. The `HibernateAssistant` interface: to provide a simple, provider-agnostic, natural-language focused API to Hibernate ORM's persistence capabilities.
18+
2. Serialization utilities: to ease the use of Hibernate ORM in the context of generative AI, for example when implementing the above.
19+
20+
No implementation is included, but the above provides the building blocks for integration with generative AI services/APIs.
21+
22+
[[assistant-gen-ai]]
23+
==== Generative AI integration considerations
24+
25+
Hibernate ORM comes with several advantages when interfacing with an LLM and accessing underlying RDBMS data, mainly:
26+
27+
Access to data is *constrained to the mapped domain model*::
28+
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;
29+
30+
Easy results consumption::
31+
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*;
32+
33+
Type-safety and query validation::
34+
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;
35+
36+
*Fail-early* in case the generated statements are incorrect::
37+
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;
38+
39+
Bridge the gap with natural language::
40+
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.
41+
42+
43+
[[assistant-serialization]]
44+
==== Serialization Components
45+
46+
To facilitate communication between Hibernate and LLM providers, the module includes two key Service Provider Interfaces (SPIs):
47+
48+
`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.
49+
50+
`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.
51+
52+
Default JSON-based implementations of both serializers are provided, offering a ready-to-use foundation for most integration scenarios.

documentation/src/main/asciidoc/userguide/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ include::chapters/beans/Beans.adoc[]
4545
include::chapters/portability/Portability.adoc[]
4646
include::chapters/statistics/Statistics.adoc[]
4747
include::chapters/tooling/Tooling.adoc[]
48+
include::chapters/assistant/Assistant.adoc[]
4849
include::appendices/BestPractices.adoc[]
4950

5051
include::Credits.adoc[]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
6+
plugins {
7+
id "local.publishing-java-module"
8+
id "local.publishing-group-relocation"
9+
}
10+
11+
description = 'Tools to integrate Hibernate with LLMs and generative AI functionalities.'
12+
13+
dependencies {
14+
api project( ':hibernate-core' )
15+
16+
testImplementation project( ':hibernate-testing' )
17+
testImplementation libs.jackson
18+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.tool.language;
6+
7+
import org.hibernate.Incubating;
8+
import org.hibernate.SharedSessionContract;
9+
import org.hibernate.query.SelectionQuery;
10+
11+
/**
12+
* Hibernate Assistant allows interacting with an underlying LLM to help you retrieve persistent data.
13+
* It leverages Hibernate ORM's mapping models, query language, cross-platform support and
14+
* built-in data restrictions to make access to information stored in relational databases
15+
* as easy as a natural language prompt.
16+
*/
17+
@Incubating
18+
public interface HibernateAssistant {
19+
/**
20+
* Creates a {@link SelectionQuery} by providing the specified natural language {@code message} to the LLM
21+
* and interpreting the obtained response.
22+
*
23+
* @param message the natural language prompt
24+
* @param session Hibernate session
25+
*
26+
* @return the {@link SelectionQuery} generated by the LLM
27+
*/
28+
default SelectionQuery<?> createAiQuery(String message, SharedSessionContract session) {
29+
return createAiQuery( message, session, null );
30+
}
31+
32+
/**
33+
* Creates a {@link SelectionQuery} by providing the specified natural language {@code message} to the LLM
34+
* and interpreting the obtained response.
35+
*
36+
* @param message the natural language prompt
37+
* @param session Hibernate session
38+
* @param resultType The {@link Class} representing the expected query result type
39+
*
40+
* @return the {@link SelectionQuery} generated by the LLM
41+
*/
42+
<T> SelectionQuery<T> createAiQuery(String message, SharedSessionContract session, Class<T> resultType);
43+
44+
/**
45+
* Prompts the underlying LLM with the provided natural language message and tries to answer it with
46+
* data extracted from the database through the persistence model.
47+
*
48+
* @param message the natural language request
49+
* @param session Hibernate session
50+
*
51+
* @return a natural language response based on the results of the query
52+
*/
53+
String executeQuery(String message, SharedSessionContract session);
54+
55+
/**
56+
* Executes the given {@link SelectionQuery}, and provides a natural language
57+
* response by passing the resulting data back to the underlying LLM.
58+
* <p>
59+
* To directly obtain a natural language response from a natural language prompt,
60+
* you can use {@link #executeQuery(String, SharedSessionContract)} instead.
61+
* <p>
62+
* If you wish to execute the query manually and obtain the structured results yourself,
63+
* you should use {@link SelectionQuery}'s direct execution methods, e.g. {@link SelectionQuery#getResultList()}
64+
* or {@link SelectionQuery#getSingleResult()}.
65+
*
66+
* @param query the AI query to execute
67+
* @param session the session in which to execute the query
68+
*
69+
* @return a natural language response based on the results of the query
70+
*/
71+
String executeQuery(SelectionQuery<?> query, SharedSessionContract session);
72+
73+
/**
74+
* Reset the assistant's current chat context. This can be helpful when
75+
* creating a new {@link SelectionQuery} that should not rely on the context
76+
* of previous requests.
77+
*/
78+
void clear();
79+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.tool.language.internal;
6+
7+
import org.hibernate.metamodel.model.domain.ManagedDomainType;
8+
import org.hibernate.tool.language.spi.MetamodelSerializer;
9+
import org.hibernate.type.format.StringJsonDocumentWriter;
10+
11+
import jakarta.persistence.metamodel.Attribute;
12+
import jakarta.persistence.metamodel.EmbeddableType;
13+
import jakarta.persistence.metamodel.EntityType;
14+
import jakarta.persistence.metamodel.IdentifiableType;
15+
import jakarta.persistence.metamodel.ManagedType;
16+
import jakarta.persistence.metamodel.MapAttribute;
17+
import jakarta.persistence.metamodel.MappedSuperclassType;
18+
import jakarta.persistence.metamodel.Metamodel;
19+
import jakarta.persistence.metamodel.PluralAttribute;
20+
import jakarta.persistence.metamodel.Type;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
28+
/**
29+
* Implementation of {@link MetamodelSerializer} that represents the {@link Metamodel} as a JSON array of mapped objects.
30+
*/
31+
public class MetamodelJsonSerializerImpl implements MetamodelSerializer {
32+
public static MetamodelJsonSerializerImpl INSTANCE = new MetamodelJsonSerializerImpl();
33+
34+
/**
35+
* Utility method that generates a JSON string representation of the mapping information
36+
* contained in the provided {@link Metamodel metamodel} instance. The representation
37+
* does not follow a strict scheme, and is more akin to natural language, as it's
38+
* mainly meant for consumption by a LLM.
39+
*
40+
* @param metamodel the metamodel instance containing information on the persistence structures
41+
*
42+
* @return the JSON representation of the provided {@link Metamodel metamodel}
43+
*/
44+
@Override
45+
public String toString(Metamodel metamodel) {
46+
final List<Map<String, Object>> entities = new ArrayList<>();
47+
final List<Map<String, Object>> embeddables = new ArrayList<>();
48+
final List<Map<String, Object>> mappedSupers = new ArrayList<>();
49+
for ( ManagedType<?> managedType : metamodel.getManagedTypes() ) {
50+
switch ( managedType.getPersistenceType() ) {
51+
case ENTITY -> entities.add( getEntityTypeDescription( (EntityType<?>) managedType ) );
52+
case EMBEDDABLE -> embeddables.add( getEmbeddableTypeDescription( (EmbeddableType<?>) managedType ) );
53+
case MAPPED_SUPERCLASS -> mappedSupers.add( getMappedSuperclassTypeDescription( (MappedSuperclassType<?>) managedType ) );
54+
default ->
55+
throw new IllegalStateException( "Unexpected persistence type for managed type [" + managedType + "]" );
56+
}
57+
}
58+
return toJson( Map.of(
59+
"entities", entities,
60+
"mappedSuperclasses", mappedSupers,
61+
"embeddables", embeddables
62+
) );
63+
}
64+
65+
private static String toJson(Map<String, Object> map) {
66+
if ( map.isEmpty() ) {
67+
return "{}";
68+
}
69+
70+
final StringJsonDocumentWriter writer = new StringJsonDocumentWriter( new StringBuilder() );
71+
toJson( map, writer );
72+
return writer.toString();
73+
}
74+
75+
private static void toJson(Object value, StringJsonDocumentWriter writer) {
76+
if ( value instanceof String strValue ) {
77+
writer.stringValue( strValue );
78+
}
79+
else if ( value instanceof Boolean boolValue ) {
80+
writer.booleanValue( boolValue );
81+
}
82+
else if ( value instanceof Number numValue ) {
83+
writer.numericValue( numValue );
84+
}
85+
else if ( value instanceof Map<?, ?> map ) {
86+
writer.startObject();
87+
for ( final var entry : map.entrySet() ) {
88+
writer.objectKey( entry.getKey().toString() );
89+
toJson( entry.getValue(), writer );
90+
}
91+
writer.endObject();
92+
}
93+
else if ( value instanceof Collection<?> collection ) {
94+
writer.startArray();
95+
for ( final var item : collection ) {
96+
toJson( item, writer );
97+
}
98+
writer.endArray();
99+
}
100+
else if ( value == null ) {
101+
writer.nullValue();
102+
}
103+
else {
104+
throw new IllegalArgumentException( "Unsupported value type: " + value.getClass().getName() );
105+
}
106+
}
107+
108+
private static void putIfNotNull(Map<String, Object> map, String key, Object value) {
109+
if ( value != null ) {
110+
map.put( key, value );
111+
}
112+
}
113+
114+
private static <T> Map<String, Object> getEntityTypeDescription(EntityType<T> entityType) {
115+
final Map<String, Object> map = new HashMap<>( 5 );
116+
map.put( "name", entityType.getName() );
117+
map.put( "class", entityType.getJavaType().getTypeName() );
118+
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) entityType ) );
119+
putIfNotNull( map, "identifierAttribute", identifierDescriptor( entityType ) );
120+
map.put( "attributes", attributeArray( entityType.getAttributes() ) );
121+
return map;
122+
}
123+
124+
private static String superTypeDescriptor(ManagedDomainType<?> managedType) {
125+
final var superType = managedType.getSuperType();
126+
return superType != null ? superType.getJavaType().getTypeName() : null;
127+
}
128+
129+
private static <T> Map<String, Object> getMappedSuperclassTypeDescription(MappedSuperclassType<T> mappedSuperclass) {
130+
final Class<T> javaType = mappedSuperclass.getJavaType();
131+
final Map<String, Object> map = new HashMap<>( 5 );
132+
map.put( "name", javaType.getSimpleName() );
133+
map.put( "class", javaType.getTypeName() );
134+
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) mappedSuperclass ) );
135+
putIfNotNull( map, "identifierAttribute", identifierDescriptor( mappedSuperclass ) );
136+
map.put( "attributes", attributeArray( mappedSuperclass.getAttributes() ) );
137+
return map;
138+
}
139+
140+
private static <T> String identifierDescriptor(IdentifiableType<T> identifiableType) {
141+
final Type<?> idType = identifiableType.getIdType();
142+
if ( idType != null ) {
143+
final var id = identifiableType.getId( idType.getJavaType() );
144+
return id.getName();
145+
}
146+
else {
147+
return null;
148+
}
149+
}
150+
151+
private static <T> Map<String, Object> getEmbeddableTypeDescription(EmbeddableType<T> embeddableType) {
152+
final Class<T> javaType = embeddableType.getJavaType();
153+
final Map<String, Object> map = new HashMap<>( 4 );
154+
map.put( "name", javaType.getSimpleName() );
155+
map.put( "class", javaType.getTypeName() );
156+
putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType<?>) embeddableType ) );
157+
map.put( "attributes", attributeArray( embeddableType.getAttributes() ) );
158+
return map;
159+
}
160+
161+
private static <T> List<Map<String, String>> attributeArray(Set<Attribute<? super T, ?>> attributes) {
162+
if ( attributes.isEmpty() ) {
163+
return List.of();
164+
}
165+
166+
return attributes.stream().map( attribute -> {
167+
final String name = attribute.getName();
168+
String type = attribute.getJavaType().getTypeName();
169+
// add key and element types for plural attributes
170+
if ( attribute instanceof PluralAttribute<?, ?, ?> pluralAttribute ) {
171+
type += "<";
172+
final var collectionType = pluralAttribute.getCollectionType();
173+
if ( collectionType == PluralAttribute.CollectionType.MAP ) {
174+
type += ( (MapAttribute<?, ?, ?>) pluralAttribute ).getKeyJavaType().getTypeName() + ",";
175+
}
176+
type += pluralAttribute.getElementType().getJavaType().getTypeName() + ">";
177+
}
178+
return Map.of(
179+
"type", type,
180+
"name", name
181+
);
182+
} ).toList();
183+
}
184+
}

0 commit comments

Comments
 (0)