- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 3.7k
          HHH-19880 Move Hibernate Tools' hibernate-assistant module to Hibernate ORM
          #11156
        
          New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Draft
      
      
            mbellade
  wants to merge
  1
  commit into
  hibernate:main
  
    
      
        
          
  
    
      Choose a base branch
      
     
    
      
        
      
      
        
          
          
        
        
          
            
              
              
              
  
           
        
        
          
            
              
              
           
        
       
     
  
        
          
            
          
            
          
        
       
    
      
from
mbellade:language-module
  
      
      
   
  
    
  
  
  
 
  
      
    base: main
Could not load branches
            
              
  
    Branch not found: {{ refName }}
  
            
                
      Loading
              
            Could not load tags
            
            
              Nothing to show
            
              
  
            
                
      Loading
              
            Are you sure you want to change the base?
            Some commits from the old base branch may be removed from the timeline,
            and old review comments may become outdated.
          
          
  
     Draft
                    Changes from all commits
      Commits
    
    
  File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
        
          
          
            52 changes: 52 additions & 0 deletions
          
          52 
        
  documentation/src/main/asciidoc/userguide/chapters/assistant/Assistant.adoc
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | 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 | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect heading level 
        Suggested change
       
 | ||||||
|  | ||||||
| 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 | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect heading level 
        Suggested change
       
 | ||||||
|  | ||||||
| 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. | ||||||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | 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 | ||
| } | 
        
          
          
            79 changes: 79 additions & 0 deletions
          
          79 
        
  hibernate-assistant/src/main/java/org/hibernate/tool/language/HibernateAssistant.java
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | 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(); | ||
| } | 
        
          
          
            184 changes: 184 additions & 0 deletions
          
          184 
        
  ...stant/src/main/java/org/hibernate/tool/language/internal/MetamodelJsonSerializerImpl.java
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | 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(); | ||
| } | ||
| } | 
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.