Skip to content
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

Classloader issue in Qute rendering of prompt templates #436

Closed
FroMage opened this issue Apr 2, 2024 · 8 comments · Fixed by #437
Closed

Classloader issue in Qute rendering of prompt templates #436

FroMage opened this issue Apr 2, 2024 · 8 comments · Fixed by #437
Labels
bug Something isn't working

Comments

@FroMage
Copy link

FroMage commented Apr 2, 2024

I have the following prompt:

@RegisterAiService( 
		retrievalAugmentor = ScheduleDocumentRetreiver.class
)
public interface ScheduleAI {

    @SystemMessage("You are a computer science conference organiser") 
    @UserMessage("""
                Help me select talks that match my favorite topics: {topics}. Give me the list of talks. 
            """)
    String findTalks(String topics); 
}

(Pretty sure the retreiver is unrelated and can be removed)

When I call my service, it fails with:

org.hibernate.query.QueryArgumentException: Argument [DISPLAY_NEW_SPEAKERS] of type [model.ConfigurationKey] did not match parameter type [model.ConfigurationKey (n/a)]
	at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:85)
	at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:32)
	at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:362)
	at org.hibernate.query.internal.QueryParameterBindingImpl.setBindValue(QueryParameterBindingImpl.java:137)
	at org.hibernate.query.spi.AbstractCommonQueryContract.setParameter(AbstractCommonQueryContract.java:926)
	at org.hibernate.query.spi.AbstractSelectionQuery.setParameter(AbstractSelectionQuery.java:924)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.setParameter(QuerySqmImpl.java:1282)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.setParameter(QuerySqmImpl.java:140)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.bindParameters(AbstractJpaOperations.java:153)
	at io.quarkus.hibernate.orm.panache.common.runtime.CommonPanacheQueryImpl.createBaseQuery(CommonPanacheQueryImpl.java:375)
	at io.quarkus.hibernate.orm.panache.common.runtime.CommonPanacheQueryImpl.createQuery(CommonPanacheQueryImpl.java:344)
	at io.quarkus.hibernate.orm.panache.common.runtime.CommonPanacheQueryImpl.firstResult(CommonPanacheQueryImpl.java:293)
	at io.quarkus.hibernate.orm.panache.runtime.PanacheQueryImpl.firstResult(PanacheQueryImpl.java:159)
	at model.Configuration.displayNewSpeakers(Configuration.java:85)
	at rest.Application$ApplicationGlobals.displayNewSpeakers(Application.java:127)
	at rest.Application$_ApplicationGlobals_Globals.accept(Unknown Source)
	at io.quarkus.qute.TemplateImpl.instance(TemplateImpl.java:56)
	at io.quarkiverse.langchain4j.QuarkusPromptTemplateFactory$QuteTemplate.render(QuarkusPromptTemplateFactory.java:57)
	at dev.langchain4j.model.input.PromptTemplate.apply(PromptTemplate.java:102)
	at io.quarkiverse.langchain4j.runtime.aiservice.AiServiceMethodImplementationSupport.prepareSystemMessage(AiServiceMethodImplementationSupport.java:257)
	at io.quarkiverse.langchain4j.runtime.aiservice.AiServiceMethodImplementationSupport.doImplement(AiServiceMethodImplementationSupport.java:96)
	at io.quarkiverse.langchain4j.runtime.aiservice.AiServiceMethodImplementationSupport.implement(AiServiceMethodImplementationSupport.java:78)
	at util.ScheduleAI$$QuarkusImpl.findTalks(Unknown Source)
	at util.ScheduleAI$$QuarkusImpl_ClientProxy.findTalks(Unknown Source)
	at rest.Application.ai(Application.java:509)
	at rest.Application_ClientProxy.ai(Unknown Source)

This is my endpoint:

@Blocking
@Path("")
public class Application {

	@CheckedTemplate
	public static class Templates {
		public static native TemplateInstance ai(String results);
	}

    @TemplateGlobal
    public static class ApplicationGlobals {
    	
    	public static String promotedPage() {
    		return Configuration.getPromotedPage();
    	}
    	public static boolean displayTalks() {
    		return Configuration.displayTalks();
    	}
    	public static boolean ticketingIsOpen() {
    		return Configuration.ticketingIsOpen();
    	}
    	public static boolean displayNewSpeakers() {
    		return Configuration.displayNewSpeakers();
    	}
    	public static boolean cfpIsOpened() {
    		return Configuration.cfpIsOpened();
    	}
    	public static String cfpUrl() {
    		return Configuration.getCfpUrl();
    	}
    }


    @Inject
    ScheduleAI ai;
    
    @GET
    @Path("/ai")
    public TemplateInstance ai(@RestQuery String topics){
    	String results = null;
    	if(topics != null && !topics.isBlank()) {
    		String something = ai.findTalks(topics);
    		Log.info("REsults: "+something);
    	}
    	return Templates.ai(results);
    }
}

I believe this is due to Qute Globals not being lazy (which I'll file a bug for) and them being called even for prompts (probably should not happen) and what looks like a ClassLoader issue, because the globals should just work, they do in normal endpoint calls, and in html template rendering.

This is the entity:

@Entity
public class Configuration extends PanacheEntity implements Comparable<Configuration> {

	@Enumerated(EnumType.STRING)
	public ConfigurationKey key;

	public String value;

	@Override
	public String toString() {
		return this.key + " : " + this.value;
	}
	
    /**
     * Retourne l'API KEY sauvée en BD. En local, si la clé n'est pas définie alors
     * la google map fonctionne quand même. MAIS en Prod/Staging, il FAUT une API
     * Key sinon la carte ne fonctionne pas c'est certainement une restriction
     * google.
     * 
     * L'API KEY de Prod ne peut pas être utilisée en local, car nous l'avons
     * restreinte pour ne fonctionner qu'avec les domaines *.rivieradev.fr et
     * *.rivieradev.com afin de suivre les recommandations de sécurité décrites par
     * Google.
     * 
     * Pour générer une nouvelle API KEY :
     * https://developers.google.com/maps/documentation/javascript/get-api-key?hl=Fr
     */
    public static String getGoogleMapApiKey() {
        Configuration config = Configuration.find("key", ConfigurationKey.GOOGLE_MAP_API_KEY).firstResult();
        if (config != null) {
            return config.value;
        }
        return null;
    }

    /**
     * Retourne la date de début de la conférence telle qu'elle est stockée en BD.
     * Elle devrait être au format ISO. Ex: 2019-05-15T08:20:00
     * 
     * @return la date de début de la conférence
     */
    public static String getEventStartDate() {
        Configuration config = Configuration.find("key", ConfigurationKey.EVENT_START_DATE).firstResult();
        if (config != null) {
            return config.value;
        }
        return null;
    }

    /**
     * Retourne la date de fin de la conférence telle qu'elle est stockée en BD.
     * Elle devrait être au format ISO. Ex: 2019-05-15T08:20:00
     * 
     * @return la date de fin de la conférence
     */
    public static String getEventEndDate() {
        Configuration config = Configuration.find("key", ConfigurationKey.EVENT_END_DATE).firstResult();
        if (config != null) {
            return config.value;
        }
        return null;
    }

    /**
     * Retourne true si le programme complet doit être affiché, faux sinon.
     */
    public static boolean displayFullSchedule() {
        Configuration config = Configuration.find("key", ConfigurationKey.DISPLAY_FULL_SCHEDULE).firstResult();
        return config != null && config.value.equals("true");
    }

    /**
     * Retourne true si les speakers de la nouvelle édition doivent être affichés
     * (utile avant que le programme définitif ne soit connu)
     */
    public static boolean displayNewSpeakers() {
        Configuration config = Configuration.find("key", ConfigurationKey.DISPLAY_NEW_SPEAKERS).firstResult();
        return config != null && config.value.equals("true");
    }

    /**
     * Retourne la page à mettre en avant sur la home page et dans le menu. 'CFP' :
     * La page du CFP 'TICKETS' : La page d'achat de tickets 'SPONSORS' : La page
     * pour devenir un sponsor
     */
    public static String getPromotedPage() {
        Configuration config = Configuration.find("key", ConfigurationKey.PROMOTED_PAGE).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Retourne la 2ème page à mettre en avant sur la home page. 'SPONSORS' : La
     * page pour devenir un sponsor 'SCHEDULE' : Le programme
     */
    public static String getPromotedPage2() {
        Configuration config = Configuration.find("key", ConfigurationKey.PROMOTED_PAGE_2).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Retourne l'Url de la page où on peut acheter les billets.
     */
    public static String getTicketingUrl() {
        Configuration config = Configuration.find("key", ConfigurationKey.TICKETING_URL).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Retourne true s'il est possible d'acheter des billets. (utile pour enlever
     * l'accès à la page de vente des billets)
     */
    public static boolean ticketingIsOpen() {
        Configuration config = Configuration.find("key", ConfigurationKey.TICKETING_OPEN).firstResult();
        return config != null && config.value.equals("true");
    }

    /**
     * Retourne l'Url de la page de l'organisme de formation.
     */
    public static String getTicketingTrainingUrl() {
        Configuration config = Configuration.find("key", ConfigurationKey.TICKETING_TRAINING_URL).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Retourne true s'il est possible d'accéder à la page de l'organisme de formation 
     * (utile en attendant que la page soit prête)
     */
    public static boolean ticketingTrainingIsOpen() {
        Configuration config = Configuration.find("key", ConfigurationKey.TICKETING_TRAINING_OPEN).firstResult();
        return config != null && config.value.equals("true");
    }

    /**
     * Return true if Call For Paper is opened, false otherwise
     */
    public static boolean cfpIsOpened() {
        Configuration config = Configuration.find("key", ConfigurationKey.CFP_OPEN).firstResult();
        return config != null && config.value.equals("true");
    }

    /**
     * Return the Call for Paper URL
     */
    public static String getCfpUrl() {
        Configuration config = Configuration.find("key", ConfigurationKey.CFP_URL).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Retourne true si le menu doit permettre d'afficher la page des talks (utile
     * pour enlever l'accès à la page tant qu'on n'a pas encore de talks)
     */
    public static boolean displayTalks() {
        Configuration config = Configuration.find("key", ConfigurationKey.DISPLAY_TALKS).firstResult();
        return config != null && config.value.equals("true");
    }

    public static String getSponsoringLeafletUrl() {
        Configuration config = Configuration.find("key", ConfigurationKey.SPONSORING_LEAFLET_URL).firstResult();
        return config != null ? config.value : null;
    }

    /**
     * Covid19 newsletter URL
     */
    public static String getCancelledUrl() {
        Configuration config = Configuration.find("key", ConfigurationKey.CANCELLED_URL).firstResult();
        return config != null ? config.value : null;
    }

	@Override
	public int compareTo(Configuration o) {
		return key.compareTo(o.key);
	}
}

And the enum in question:

public enum ConfigurationKey {
    GOOGLE_MAP_API_KEY,
    /* date de début de la conf (format ISO. Ex: 2019-05-15T08:20:00) */
    EVENT_START_DATE,
    /* date de fin de la conf (format ISO. Ex: 2019-05-17T18:00:00) */
    EVENT_END_DATE,
    /*
     * [true, false] Si true, affiche le programme définitif à la place du programme
     * TemporarySlots
     */
    DISPLAY_FULL_SCHEDULE,
    /*
     * [true, false] Si true, affiche les speakers de la nouvelle édition (utile
     * avant que le programme définitif ne soit connu)
     */
    DISPLAY_NEW_SPEAKERS,
    /*
     * [true, false] Si true, affiche dans le menu l'item Talks (utile avant que les
     * talks n'ont pas encore été choisis)
     */
    DISPLAY_TALKS,
    /*
     * [CFP, TICKETS, SPONSORS] Permet de mettre en avant une page spécifique dans
     * la home page et dans le menu
     */
    PROMOTED_PAGE,
    /*
     * [SPONSORS, SCHEDULE] Permet de mettre en avant une 2ème page spécifique dans
     * la home page au dessus du compteur
     */
    PROMOTED_PAGE_2,
    /* Url de la page où on peut acheter les billets */
    TICKETING_URL,
    /* [true, false] Si true, permet d'accéder à la billeterie */
    TICKETING_OPEN,
    /* Url de la page de l'organisme de formation */
    TICKETING_TRAINING_URL,
    /* [true, false] Si true, permet d'accéder à la page de l'organisme de formation */
    TICKETING_TRAINING_OPEN,
    /* URL de la plaquette de sponsoring */
    SPONSORING_LEAFLET_URL,
    /* [true, false] true if the Call For Paper is opened, false otherwise */
    CFP_OPEN,
    /* Call For Paper URL */
    CFP_URL,
    /* Temporary for Covid19, unsed in the home page */
    CANCELLED_URL;
}
@geoand
Copy link
Collaborator

geoand commented Apr 2, 2024

Thanks for the detailed report!

I was not even aware of the template globals.

Do you mind attaching your sample so I can play around with it?

@FroMage
Copy link
Author

FroMage commented Apr 2, 2024

I'll make you a reproducer.

@geoand geoand added the bug Something isn't working label Apr 2, 2024
@geoand
Copy link
Collaborator

geoand commented Apr 2, 2024

🙏🏼

@FroMage
Copy link
Author

FroMage commented Apr 2, 2024

OK, so, it's worse, it's a hot reload class loader issue.

  1. Clone https://github.com/FroMage/langhchain-qute-cl
  2. Add your OpenAI key to application.properties.
  3. Run with mvn clean quarkus:dev
  4. Go to http://localhost:8080/hello (it should work, useless output, but no exception)
  5. Edit GreetingResource replace bla bli with bla
  6. Go to http://localhost:8080/hello you should get the exception

@geoand
Copy link
Collaborator

geoand commented Apr 2, 2024

Thanks, I'll probably have a look tomorrow

geoand added a commit that referenced this issue Apr 4, 2024
Ensure that templates don't cause CL issues during reload
@FroMage
Copy link
Author

FroMage commented Apr 5, 2024

Thanks a lot

@geoand
Copy link
Collaborator

geoand commented Apr 5, 2024

Np!

0.10.3 is already out and contains the fix

@FroMage
Copy link
Author

FroMage commented Apr 5, 2024

Even better !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants