chainlit icon indicating copy to clipboard operation
chainlit copied to clipboard

Make browser query parameters available in `@on_chat_start`

Open kevinwmerritt opened this issue 2 years ago • 22 comments

Feature request for query params to be made available in the on_chat_start decorator. From there you could add them to user session or take another action.

@on_chat_start
async def main(query_params):
    document_url = query_params.get('document_url')
    if document_url:
        ....

Imagine an application that lists multiple documents and you want to design a chainlit that can Q&A one of those documents.

Possible workflow today:

  1. The user downloads the document from the application.
  2. In a separate browser tab they open the chainlit.
  3. In the chainlit they upload the file with a AskFileMessage dialog.
  4. Process the file and chat.

Possible workflow enabled by query params:

  1. User clicks a [Chat] button next the document they want to Q&A and a new browser tab opens to mychainlit.xyz?document_url=/my/doc.pdf
  2. The chainlit begins processing the file referenced in the document.

Additional scenarios could include:

  • passing a reference to an ID to an API accessible resource
  • chaining chainlit apps by passing fragments of one chat to another chainlit as part of a larger workflow

kevinwmerritt avatar Jul 07 '23 00:07 kevinwmerritt

I would very much like to support @kevinwmerritt's idea—I think it's a feature with a big impact.

andreasmartin avatar Dec 01 '23 08:12 andreasmartin

Noted! Will try to ship that in the 1.0.0 release

willydouhard avatar Dec 01 '23 10:12 willydouhard

@willydouhard do you have any updates on this matter? thanks a lot

laurentiupiciu avatar Jan 10 '24 18:01 laurentiupiciu

Did not have the time to add it yet. I am unsure about the behaviour though. Let's say you connect to http://localhost:8000?params={...}. In this case the json would probably encoded in b64 but that's not the point.

So we send that to the socket connection headers. Now let's say I navigate in the app a few times and create a new session. Is it expected that the params are sent again? In that case that means we should store them in the local storage or something. Also what happens if I refresh the page without the ?params?

willydouhard avatar Jan 11 '24 08:01 willydouhard

from my perspective it should be something like in streamlit: https://docs.streamlit.io/library/api-reference/utilities/st.experimental_get_query_params

what's your opinion on this?

laurentiupiciu avatar Jan 11 '24 10:01 laurentiupiciu

I think it works well with Streamlit since you have more freedom and control over the app. In Chainlit these url parameters will disappear as the user uses the app.

To be clear I think this is a good feature but I don't have the right design atm.

willydouhard avatar Jan 11 '24 13:01 willydouhard

@willydouhard just give either the request object or at least the GET/POST/Headers as optional param to the@cl.on_chat_start method. I guess that involves forwarding the data from python to react when loading the page, and then back react to python for the handlers..

Alternatively or additionally, do the same here:

@app.post("/auth/header")
async def header_auth(request: Request):
    if not config.code.header_auth_callback:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No header_auth_callback defined",
        )

    app_user = await config.code.header_auth_callback(request.headers, add the request here)
    ...

PS: I just tried to use cookies, but of course, with an <iframe> and Apple, that's not working at all... A bit at a loss now..

Update: We now pass the needed GET param as a header to chainlit, which involves a) overriding the /login route to inject a JS into the frontend, which in turn overrides the JS fetch() method to add the needed header with our parameter. And then we get it finally where we need it to create the users, in our header_auth_callback().

If anyone needs more details, let me know. Of course we hope for a native clean solution - but at least we managed to not do a vendor code hack here.

bachi76 avatar Feb 02 '24 15:02 bachi76

This kind of feature would be super useful for creating links with pre-populated chats, which is somewhat similar to the document Id scenario above

For instance in an app you could embed links where the user clicks them and it launches your bot with the user's input entered ready for them to submit (or to add to if they need something extra in their input). This takes some friction out of the interaction. In some cases you could imagine dynamically creating the link parameters too (eg in Excel you could have a list of company names and each row gets a custom link to ask for more about that particular company using Excel's HYPERLINK function to build the link).

I was able to get close to this with a few hacky lines of custom js to write a parameter value into the user input box but I haven't the React experience to have that feed into the message properly (so the input box quickly gets overwritten with blank text unless the user specifically types something in the input box immediately, which isn't a good user experience).

nmstoker avatar Mar 12 '24 17:03 nmstoker

I need this feature

comediansd avatar Mar 24 '24 05:03 comediansd

Update: We now pass the needed GET param as a header to chainlit, which involves a) overriding the /login route to inject a JS into the frontend, which in turn overrides the JS fetch() method to add the needed header with our parameter. And then we get it finally where we need it to create the users, in our header_auth_callback().

If anyone needs more details, let me know. Of course we hope for a native clean solution - but at least we managed to not do a vendor code hack here.

@bachi76 Please share:)

AlaaSenjab avatar Mar 28 '24 07:03 AlaaSenjab

I have found a solution that might work for others as well.

Let's assume we have the following URL with query parameter: https://xyz.ch/?agent_id=myAgentId

First, I define a custom_js with the following content:

window.addEventListener("chainlit-call-fn", (e) => {
  const { name, args, callback } = e.detail;
  if (name === "url_query_parameter") {
    callback((new URLSearchParams(window.location.search)).get(args.msg));
  }
});

And then I can use this CopilotFunction in Chainlit as follows, for example:

@cl.on_chat_start
async def main():
    agent_id = await cl.CopilotFunction(name="url_query_parameter", args={"msg": "agent_id"}).acall()

andreasmartin avatar Apr 23 '24 10:04 andreasmartin

Would love to add this in the docs examples or advanced concepts. Do you have time for this @andreasmartin ?

willydouhard avatar Apr 24 '24 09:04 willydouhard

This is broken since Chainlit 1.1.300. Can anyone help me out with fastapi code that "send" the url parameter to the chainlit module?

comediansd avatar Jul 05 '24 08:07 comediansd

I can confirm this is broken, and the changes are not mentioned in the documentation.

CallumCM avatar Jul 08 '24 22:07 CallumCM

I agree, this is really a needed feature.. There needs to be some way to send arbitrary parameters to a chainlit app, and there currently is not anything that doesn't require extensive hacking.

For others looking for a workaround, I am currently doing this, but I'm not sure how reliable this is at all as it is very much a hack...

Basically, I see that chainlit, at least in my setup, passes the "referrer" header from the browser when doing the authentication. Assuming your chainlit app is running at the path /chat, if you direct a user to https://example.com/chat/login?arg1=arg1&arg2=arg2 and then have code something like this:

from chainlit import Message, User
from typing import Dict, Optional
from urllib.parse import urlparse, parse_qs
import chainlit as cl

@cl.header_auth_callback
async def header_auth_callback(headers: Dict) -> Optional[User]:
    user = None

    referrer = headers.get('referer')

    if referrer:
        parsed_url = urlparse(referrer)
        query_params = parse_qs(parsed_url.query)

        payload = dict(
            ...
        )
        
        user = User(
                identifier='asldkfj',
                metadata=payload,
        )

    return user

@cl.on_chat_start
async def chat_start():
    logger.info(f'Chat started.')
    user = cl.user_session.get("user")

    await Message(
        content=user.metadata,
    ).send()

You can basically, during your authentication hook, just add whatever you want from the query string or headers into your user payload. You can even just do this without needing to do anything further or for actual authentication.

This is at least slightly elegant, mainly because it is pure python.

marty-sullivan avatar Jul 13 '24 17:07 marty-sullivan

Some good ideas/workarounds here. I see several users trying to do this in discord forum as well. I am trying to address a similar need where I am trying to create a no-code agent. Chainlit application acts as generic agent driver, accepts agent_id as path/query parameter. And in on_chat_start, generic agent driver can pull in model, starter_prompts, system prompt, any tools etc, setup langchain and continue. All this works, except getting access to agent_id from the path.

I posted this in discord yesterday https://discord.com/channels/1088038867602526210/1126876648839598130/1260866911298781214

I wanted a solution with no hacks at all. So, I made some minor changes to chainlit backend today to achieve this. Essentially, when a client connects, I am extracting PATH_INFO from WSGI environ and adding it to WebsocketSession along with other attributes that already exist. And then, added it to UserSession as well. Same thing can be extended to query parameter with couple of more lines.

I just tested this and I can get "http_path" from session object and can extract agent_id from the path. Planning to raise a PR soon. In the meantime, here is my commit to the fork https://github.com/nethi/chainlit/commit/c90b4a5f6cbd87e4c1126ac83a2b07683883d267

I need similar solution to other non-websocket related REST APIs and chainlit hooks, where access to Request object is requried . For example, I want to agent_id specific profiles to be dispalyed - I am not familiar with FastAPI, but flask has a nice way to get to the context object without explicitly passing in in the arguments. It would be nice to make Request object (or some attributes likes path, query, headers etc) made available in these chainlit hooks. I believe, FastAPI context var could be a solution- still looking around

Chainlit is a wonderful framework. Great work and thank you @willydouhard

nethi avatar Jul 14 '24 05:07 nethi

There is a very easy but undocumented solution to this as of 1.1.0 - the User Session docs are incomplete but per the release notes the object now includes the "http referer" [sic].

Thus, the following works:

@cl.on_chat_start
async def start():

    path = cl.user_session.get("http_referer")

    # get query string from path
    query_string = urllib3.util.parse_url(path).query

    print(query_string)

This should be added to the docs and the bug reports/feature requests closed, IMO. Kicking myself for not figuring this out quicker - IMO it is poorly named because while in-spec (the resource is being used on the "refering" page) it is not, for example, going to give you the actual referrer (eg news.ycombinator.com if someone clicks a link to your agent from HN). I don't think there needs to be a separate query strings parameter because, per example above, extracting that is trivial given the "referer."

francisjervis avatar Jul 15 '24 00:07 francisjervis

I observed that cl.user_session.get("http_referer") returns None if I start the UI in browser by typing in the address. But this may be useful in other scenarios where http_referer returns valid value.

nethi avatar Jul 15 '24 06:07 nethi

I would indeed be concerned that this might not work during the login flow too, since the query string may be lost during the redirects, as described as a limiter at the beginning of this issue. I don't know whether that is the case or not, but that is why I built my workaround into the login hook, and link users directly to the login endpoint, rather than looking elsewhere.

marty-sullivan avatar Jul 15 '24 14:07 marty-sullivan

Some good ideas/workarounds here. I see several users trying to do this in discord forum as well. I am trying to address a similar need where I am trying to create a no-code agent. Chainlit application acts as generic agent driver, accepts agent_id as path/query parameter. And in on_chat_start, generic agent driver can pull in model, starter_prompts, system prompt, any tools etc, setup langchain and continue. All this works, except getting access to agent_id from the path.

I posted this in discord yesterday discord.com/channels/1088038867602526210/1126876648839598130/1260866911298781214

I wanted a solution with no hacks at all. So, I made some minor changes to chainlit backend today to achieve this. Essentially, when a client connects, I am extracting PATH_INFO from WSGI environ and adding it to WebsocketSession along with other attributes that already exist. And then, added it to UserSession as well. Same thing can be extended to query parameter with couple of more lines.

I just tested this and I can get "http_path" from session object and can extract agent_id from the path. Planning to raise a PR soon. In the meantime, here is my commit to the fork nethi@c90b4a5

I need similar solution to other non-websocket related REST APIs and chainlit hooks, where access to Request object is requried . For example, I want to agent_id specific profiles to be dispalyed - I am not familiar with FastAPI, but flask has a nice way to get to the context object without explicitly passing in in the arguments. It would be nice to make Request object (or some attributes likes path, query, headers etc) made available in these chainlit hooks. I believe, FastAPI context var could be a solution- still looking around

Chainlit is a wonderful framework. Great work and thank you @willydouhard

I made some more changes to the fork

  • added support for accessing query parameters
  • An option to pass a separate path for socket.io path

WIth these changes, I now have a chainlit based generic agent driver that can accept an agent_id in the URL (can also be query parameter), call out to another service to get details of the agent (given the agent id), containing details like model profiles, starter prompts, system prompt etc. With this, it is now possible to build a fully capable no-code agent (like that of custom GPT assistant, Hugging face assistatnst), all using chainlit framework.

nethi avatar Jul 21 '24 05:07 nethi

I also need this feature, hope it can be added in the future

Evanrsl avatar Jul 24 '24 02:07 Evanrsl

+1 for requesting this feature. in the meantime, does anyone recommend another supported method for providing user metadata or native app context to the chat web app without having to make the user provide it (e.g. user profile info)?

bfinamore avatar Aug 07 '24 14:08 bfinamore

I've created a PR https://github.com/Chainlit/chainlit/pull/1239 for this, thanks to the many suggestions in this thread. Pls help to test/review. A working version can be seen here: https://2in.ai/

qtangs avatar Aug 19 '24 12:08 qtangs

I'm closing this in favour of #1213, which would make the entire request object (including cookies, URL, headers) available. Happy to receive PR's for that one and hereby explicitly offering guidance/bike-shedding on spec'ing the feature.

dokterbob avatar Aug 26 '24 09:08 dokterbob