阅读原文
建议阅读原文,始终查看最新文档版本,获得最佳阅读体验:《基于知识库的AI客服系统开发和实施文档》
前言
本文详细说明如何实施我开发的AI客服系统
项目总结报告
源代码
代码已开源,github项目网址为:iamtornado/AI-Assistant
开发环境
Windows11系统,用trae国内版(基于vs code)开发
python版本:3.13
技术选型
我选择了以下技术栈:
前端与应用框架: Chainlit
一个用于快速构建聊天应用的Python框架,提供了用户认证、会话管理和友好的UI。其支持实时渲染,比如,消息中如果有代码,则会自动显示代码块,方便阅读。
知识库与问答引擎: RAGFlow
领先的检索增强生成(RAG)平台,负责知识的存储、检索和智能问答。其还支持多语言、MCP、agent等。
人工客服平台: Rocket.Chat
一个功能强大的开源团队沟通协作平台,用作人工客服的工作台。
消息队列: Redis
利用其发布/订阅和Stream数据结构,实现各服务间的异步消息通信,确保系统的响应速度和稳定性。
Web服务: FastAPI
用于构建接收Rocket.Chat消息的Webhook服务,性能卓越。
身份认证: Keycloak
通过OAuth 2.0协议,为Chainlit提供统一、安全的用户身份认证服务。keycloak对接的是企业LDAP,因此用户可以直接通过现有的域账号登录AI客服系统。
系统架构图
总体架构图
时序图
以上图形是利用 DiagramGPT(eraser) 生成的
前端(chainlit)
安装chainlit
Linux系统
首先要安装python和pip,此过程省略
注意,下面几行命令是在ubuntu desktop 24.04系统上运行的,Windows系统命令有所区别
#创建虚拟环境
python3 -m venv chainlit
#激活虚拟环境
source ./chainlit/bin/activate
#用pip安装chainlit包
pip install -U chainlit
Windows系统,trae(vs code)
先通过vs code创建一个空的app.py文件,然后通过命令面板创建虚拟环境
成功创建虚拟环境后,就可以在资源管理中看到自动生成了.env文件夹
激活虚拟环境
.venv\Scripts\activate
检查当前python解释器是不是虚拟环境中的解释器,如果不是,务必选择虚拟环境中的解释器
用pip安装chainlit包,时间会比较长,因为国内下载包会比较慢
pip install --upgrade chainlit
初步了解chainlit–编写极简应用逻辑
参考资料:In Pure Python - Chainlit
如果你对chainlit已经有了初步了解,可以直接看下一小节。
这是一个非常简单的应用,就是会将用户的输入再返回,前面加上Received:
import chainlit as cl
@cl.on_message
async def main(message: cl.Message):
# Your custom logic goes here...
# Send a response back to the user
await cl.Message(
content=f"Received: {message.content}",
).send()
设置环境变量(API key)
下面这种方式是用于临时设置环境变量
$env:OPENAI_API_KEY = "<替换为你的API key>"
官方建议用.env文件设置环境变量,官方文档为Environment Variables - Chainlit
运行应用
在终端中导航到app.py文件所在的文件夹,然后运行命令:
chainlit run app.py -w
下图是Linux系统中的截图
会自动打开浏览器
下面是Windows系统中trae的截图,也会自动在Windows系统默认浏览器中打开网页
也可以通过trae的webview功能直接预览,更加方便开发调试
实现对接LDAP(存在问题,请勿阅读)
本小节的内容是我在做实验时记录的,存在问题,请直接阅读下一小节,了解如何将keycloak对接企业LDAP(Microsoft AD)
部署keycloak
参考资料:Docker - Keycloak
:::
我在k8s部署keycloak时(无论是helmkeycloak 24.4.12 · bitnami/bitnami,还是官方的教程Kubernetes - Keycloak),发现一个问题,用web登录,一直提示“We are sorry… HTTPS required”,但是用docker部署没有这个问题。
我想应该是这两种方式强制要求用https
官方还提供了通过operator的方式部署keycloak,我没有试过。Keycloak Operator Installation - Keycloak
https://operatorhub.io/operator/keycloak-operator
:::
用docker部署(无持久存储)
不建议用下面的方式部署keycloak,重启容器后,所有数据都会丢失
#注意,下行命令中的--dns=192.168.124.7参数含义为让容器使用此dns,如果不指定,则容器内的程序无法访问企业内部网络,从而导致无法进行dns解析,在连接LDAP时会报错
docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --dns=192.168.124.7 docker.1ms.run/keycloak/keycloak:26.2.5 start-dev
用docker部署(持久存储)
对于开发环境和测试环境,建议用下面的方法部署keycloak,数据不会丢失,保存在本地(以文件形式)
mkdir -p /opt/keycloak/data
sudo chmod -R a+rwx /opt/keycloak/data
docker run -d \
--name keycloak \
-p 8080:8080 \
-e KC_DB=dev-file \
-v /opt/keycloak/data:/opt/keycloak/data \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
docker.1ms.run/keycloak/keycloak:26.2.5 start-dev
:::
如果出现下面的错误日志,则说明容器或者说是keycloak程序无法解析域名解析,需要调整dns,使其能解析指定域名
2025-03-12 11:50:46,712 ERROR [org.keycloak.services] (executor-thread-17) KC-SERVICES0055: Error when connecting to LDAP: dc-t.dltornado2.com:389: javax.naming.CommunicationException: dc-t.dltornado2.com:389 [Root exception is java.net.UnknownHostException: dc-t.dltornado2.com]
at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:251)
at java.naming/com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:141)
at java.naming/com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1620)
at java.naming/com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2848)
at java.naming/com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:349)
at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxFromUrl(LdapCtxFactory.java:229)
at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:189)
at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:247)
at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:154)
at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:84)
at java.naming/javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:520)
at java.naming/javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305)
at java.naming/javax.naming.InitialContext.init(InitialContext.java:236)
at java.naming/javax.naming.ldap.InitialLdapContext.<init>(InitialLdapContext.java:154)
at org.keycloak.storage.ldap.idm.store.ldap.LDAPContextManager.createLdapContext(LDAPContextManager.java:81)
at org.keycloak.storage.ldap.idm.store.ldap.LDAPContextManager.getLdapContext(LDAPContextManager.java:106)
at org.keycloak.services.managers.LDAPServerCapabilitiesManager.testLDAP(LDAPServerCapabilitiesManager.java:202)
at org.keycloak.services.resources.admin.TestLdapConnectionResource.testLDAPConnection(TestLdapConnectionResource.java:92)
at org.keycloak.services.resources.admin.TestLdapConnectionResource$quarkusrestinvoker$testLDAPConnection\_d65ae9343a71f2736595679e81594225f9de5d6f.invoke(Unknown Source)
at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:635)
at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.net.UnknownHostException: dc-t.dltornado2.com
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:751)
at java.naming/com.sun.jndi.ldap.Connection.createConnectionSocket(Connection.java:340)
at java.naming/com.sun.jndi.ldap.Connection.createSocket(Connection.java:283)
at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:230)
... 29 more
:::
然后通过web访问,网址是:http://server_ip:8080
需要登录,用户名是admin 密码是一样的,这是来自于上面的命令,设置了环境变量,可以更改环境变量。
添加一个provider
参考资料:Server Administration Guide
测试LDAP连接:
测试认证:
其它选项填写示例:
Keycloak OAuth客户端配置
在Realm中创建新Client
验证刚刚创建的client是否成功
参考资料:Docker - Keycloak
查询各个endpoint url的值,与keycloak连接的应用需要用到这些endpoint url
参考资料:OAuth - Chainlit
Invoke-RestMethod -Uri http://10.65.37.239:8080/realms/master/.well-known/openid-configuration
Invoke-RestMethod -Uri https://keycloak-admin.dltornado2.com/realms/master/.well-known/openid-configuration
从上面的输出,我们可以知道,对于chainlit,需要配置的环境变量的值为
OAUTH_AUTHORIZATION_URL=http://10.65.37.239:8080/realms/master/protocol/openid-connect/auth
OAUTH_TOKEN_URL=http://10.65.37.239:8080/realms/master/protocol/openid-connect/token
OAUTH_USERINFO_URL=http://10.65.37.239:8080/realms/master/protocol/openid-connect/userinfo
OAUTH_KEYCLOAK_CLIENT_ID=chainlit-client
#下方的环境变量的值可以通过chainlit.exe create-secret这个命令生成
CHAINLIT_AUTH_SECRET="t@k?3LolU1X%3X.PHL@_UDpQ6tf4Cg@wFbjwS^0VOy5iZl2>QP*KEikp:?Y6Pely"
设置环境变量
参考资料:OAuth - Chainlit
请使用.env文件设置环境变量,而不是通过终端命令
注意:下面第二行,等号两边不能有空格,否则执行程序时会将环境变量设置为none
# OAUTH_KEYCLOAK_BASE_URL = "http://10.65.37.239:8080"
OAUTH_KEYCLOAK_BASE_URL=http://10.65.37.239:8080
OAUTH_KEYCLOAK_REALM = "master"
OAUTH_KEYCLOAK_CLIENT_ID = "chainlit-client"
OAUTH_KEYCLOAK_CLIENT_SECRET = "t@k?3LolU1X%3X.PHL@_UDpQ6tf4Cg@wFbjwS^0VOy5iZl2>QP*KEikp:?Y6Pely" # 这里需要你填入实际的客户端密钥
OAUTH_KEYCLOAK_NAME = "chainlit-Keycloak"
OAUTH_KEYCLOAK_CLIENT_ID
OAUTH_KEYCLOAK_CLIENT_SECRET
OAUTH_KEYCLOAK_REALM
OAUTH_KEYCLOAK_BASE_URL
OAUTH_KEYCLOAK_NAME
实验时,我发现在输入用户名密码后,页面报错了,如下图:
分析日志后,是因为chainlit向keycloak发送请求时使用的是http而不是https,必须要使用https才行,因此需要对于keycloak配置证书以使用https
后续研究,发现,不是必须要用https,用http也是可以的,出现问题的原因其实是环境变量设置错误,请参考此文:《chainlit身份验证方案oauth2.0及常用社交应用账户集成》
以下是chainlit的部分日志:
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\applications.py", line 113, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 187, in __call__
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\cors.py", line 85, in __call__
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 715, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 735, in app
await route.handle(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 288, in handle
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 76, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 73, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 301, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 212, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\server.py", line 637, in oauth_callback
token = await provider.get_token(code, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\oauth_providers.py", line 715, in get_token
response = await client.post(
^^^^^^^^^^^^^^^^^^
...<2 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1859, in post
return await self.request(
^^^^^^^^^^^^^^^^^^^
...<13 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1540, in request
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1629, in send
response = await self._send_handling_auth(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1657, in _send_handling_auth
response = await self._send_handling_redirects(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1694, in _send_handling_redirects
response = await self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1730, in _send_single_request
response = await transport.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 393, in handle_async_request
with map_httpcore_exceptions():
~~~~~~~~~~~~~~~~~~~~~~~^^
File "C:\Program Files\Python313\Lib\contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.ReadTimeout
INFO: 192.168.124.11:61808 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 192.168.124.11:65425 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.124.11:65425 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:65426 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:65429 - "GET /project/translations?language=zh-CN HTTP/1.1" 200 OK
INFO: 192.168.124.11:65429 - "GET /login HTTP/1.1" 200 OK
INFO: 192.168.124.11:65429 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:65426 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:65425 - "GET /project/translations?language=zh-CN HTTP/1.1" 200 OK
INFO: 192.168.124.11:65425 - "GET /auth/oauth/chainlit-Keycloak HTTP/1.1" 307 Temporary Redirect
INFO: 192.168.124.11:65487 - "GET /user HTTP/1.1" 401 Unauthorized
2025-03-14 19:43:19 - HTTP Request: POST http://10.65.37.239:8080/realms/master/protocol/openid-connect/token "HTTP/1.1 502 Bad Gateway"
INFO: 192.168.124.11:65425 - "GET /auth/oauth/chainlit-Keycloak/callback?state=cY.Z%3AJl3eKdK%2Fg96248L8ef?lTXrEemD&session_state=fd05d8be-8824-423e-a46a-812dd3153207&iss=https%3A%2F%2Fptop.only.wip.la%3A443%2Fhttp%2F10.65.37.239%3A8080%2Frealms%2Fmaster&code=1e1ad80e-2a61-40e7-8d9a-af4c6b3890e1.fd05d8be-8824-423e-a46a-812dd3153207.5a90b396-a6c7-4708-a12e-b5351668252e HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 403, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\applications.py", line 113, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 187, in __call__
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\cors.py", line 85, in __call__
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 715, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 735, in app
await route.handle(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 288, in handle
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 76, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 73, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 301, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 212, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\server.py", line 637, in oauth_callback
token = await provider.get_token(code, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\oauth_providers.py", line 719, in get_token
response.raise_for_status()
~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_models.py", line 829, in raise_for_status
raise HTTPStatusError(message, request=request, response=self)
httpx.HTTPStatusError: Server error '502 Bad Gateway' for url 'http://10.65.37.239:8080/realms/master/protocol/openid-connect/token'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
下面是keycloak中出现的日志:
Non-secure context detected; cookies are not secured
对接企业LDAP
将keycloak与LDAP集成后,用户就可以直接通过企业内部的域账号登录应用了。
详细步骤请参考此文:《chainlit身份验证方案oauth2.0及常用社交应用账户集成》
keycloak故障诊断
internal server error
用浏览器访问,登录账户后,提示“internal server error”,chainlit的日志如下,其中关键是“httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate”
这说明证书验证出现了问题,无法找到本地的root ca
chainlit.exe run .\text_to_SQL.py -d --host 192.168.124.11
2025-06-09 15:01:05 - Loaded .env file
https://keycloak-admin.dltornado2.com
master
chainlit-client
S=l@E.9ONQ~66XggeZZdhYn.ti=3%:kM5U7hGlH1y=T4gK1IoFBZgS@1metLI_T~
chainlit-Keycloak
INFO: Started server process [4380]
INFO: Waiting for application startup.
2025-06-09 15:01:08 - Your app is available at http://192.168.124.11:8000
INFO: Application startup complete.
INFO: Uvicorn running on http://192.168.124.11:8000 (Press CTRL+C to quit)
INFO: 192.168.124.11:63143 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.124.11:63143 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63144 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:63161 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63161 - "GET /login HTTP/1.1" 200 OK
INFO: 192.168.124.11:63161 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:63144 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63143 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63251 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63273 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63274 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63274 - "GET /auth/oauth/chainlit-Keycloak HTTP/1.1" 307 Temporary Redirect
INFO: 192.168.124.11:63274 - "GET /auth/oauth/chainlit-Keycloak/callback?state=h%25%2FcOw_eex9gdLW?7.agzb8?tVia4%25_%24&session_state=cf707a93-9532-4a49-adfc-f7650aa7cbaf&iss=https%3A%2F%2Fptop.only.wip.la%3A443%2Fhttps%2Fkeycloak-admin.dltornado2.com%2Frealms%2Fmaster&code=b0360efd-30fd-4d0b-a3fa-3a3fc4db9ede.cf707a93-9532-4a49-adfc-f7650aa7cbaf.6cc6b113-468a-4426-852a-b0cf586ed621 HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
yield
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 394, in handle_async_request
resp = await self._pool.handle_async_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection_pool.py", line 256, in handle_async_request
raise exc from None
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection_pool.py", line 236, in handle_async_request
response = await connection.handle_async_request(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pool_request.request
^^^^^^^^^^^^^^^^^^^^
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 101, in handle_async_request
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 78, in handle_async_request
stream = await self._connect(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 156, in _connect
stream = await stream.start_tls(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_backends\anyio.py", line 67, in start_tls
with map_exceptions(exc_map):
~~~~~~~~~~~~~~^^^^^^^^^
File "C:\Program Files\Python313\Lib\contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
raise to_exc(exc) from exc
httpcore.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 403, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\applications.py", line 113, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 187, in __call__
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\cors.py", line 85, in __call__
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 715, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 735, in app
await route.handle(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 288, in handle
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 76, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 73, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 301, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 212, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\server.py", line 641, in oauth_callback
token = await provider.get_token(code, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\oauth_providers.py", line 715, in get_token
response = await client.post(
^^^^^^^^^^^^^^^^^^
...<2 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1859, in post
return await self.request(
^^^^^^^^^^^^^^^^^^^
...<13 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1540, in request
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1629, in send
response = await self._send_handling_auth(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1657, in _send_handling_auth
response = await self._send_handling_redirects(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1694, in _send_handling_redirects
response = await self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1730, in _send_single_request
response = await transport.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 393, in handle_async_request
with map_httpcore_exceptions():
~~~~~~~~~~~~~~~~~~~~~~~^^
File "C:\Program Files\Python313\Lib\contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)
INFO: 192.168.124.11:63273 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 192.168.124.11:63409 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.124.11:63409 - "GET /assets/index-B28WSRhf.js HTTP/1.1" 200 OK
INFO: 192.168.124.11:63411 - "GET /assets/index-BIhgNEQJ.css HTTP/1.1" 200 OK
INFO: 192.168.124.11:63467 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:63468 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63469 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63469 - "GET /favicon HTTP/1.1" 200 OK
INFO: 192.168.124.11:63469 - "GET /login HTTP/1.1" 200 OK
INFO: 192.168.124.11:63469 - "GET /logo?theme=dark& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63543 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63544 - "GET /auth/config HTTP/1.1" 200 OK
INFO: 192.168.124.11:63545 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63545 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63544 - "GET /project/translations?language=zh-CN& HTTP/1.1" 200 OK
INFO: 192.168.124.11:63544 - "GET /user HTTP/1.1" 401 Unauthorized
INFO: 192.168.124.11:63544 - "GET /auth/oauth/chainlit-Keycloak HTTP/1.1" 307 Temporary Redirect
INFO: 192.168.124.11:63786 - "GET /auth/oauth/chainlit-Keycloak/callback?state=D8myarZNlKfEo%2CzX-0Ln%25FV9%5E%3A8WT%3APo&session_state=c9da7e34-505d-48f8-8aa5-8d47e1ebe757&iss=https%3A%2F%2Fptop.only.wip.la%3A443%2Fhttps%2Fkeycloak-admin.dltornado2.com%2Frealms%2Fmaster&code=12e9e7a0-1f84-4f8a-92e7-e296434c9983.c9da7e34-505d-48f8-8aa5-8d47e1ebe757.6cc6b113-468a-4426-852a-b0cf586ed621 HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
yield
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 394, in handle_async_request
resp = await self._pool.handle_async_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection_pool.py", line 256, in handle_async_request
raise exc from None
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection_pool.py", line 236, in handle_async_request
response = await connection.handle_async_request(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pool_request.request
^^^^^^^^^^^^^^^^^^^^
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 101, in handle_async_request
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 78, in handle_async_request
stream = await self._connect(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_async\connection.py", line 156, in _connect
stream = await stream.start_tls(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_backends\anyio.py", line 67, in start_tls
with map_exceptions(exc_map):
~~~~~~~~~~~~~~^^^^^^^^^
File "C:\Program Files\Python313\Lib\contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
raise to_exc(exc) from exc
httpcore.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 403, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\applications.py", line 113, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 187, in __call__
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\cors.py", line 85, in __call__
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 715, in __call__
await self.middleware_stack(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 735, in app
await route.handle(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 288, in handle
await self.app(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 76, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\starlette\routing.py", line 73, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 301, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\fastapi\routing.py", line 212, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\server.py", line 641, in oauth_callback
token = await provider.get_token(code, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\chainlit\oauth_providers.py", line 715, in get_token
response = await client.post(
^^^^^^^^^^^^^^^^^^
...<2 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1859, in post
return await self.request(
^^^^^^^^^^^^^^^^^^^
...<13 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1540, in request
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1629, in send
response = await self._send_handling_auth(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1657, in _send_handling_auth
response = await self._send_handling_redirects(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1694, in _send_handling_redirects
response = await self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_client.py", line 1730, in _send_single_request
response = await transport.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 393, in handle_async_request
with map_httpcore_exceptions():
~~~~~~~~~~~~~~~~~~~~~~~^^
File "C:\Program Files\Python313\Lib\contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "d:\tornadofiles\scripts_脚本\github_projects\DreamAI-AI助手\.venv\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)
INFO: 192.168.124.11:63790 - "GET /favicon.ico HTTP/1.1" 200 OK
编写chainlit应用逻辑
主要是chainlit_ragflow_streaming.py
文件,其完整代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Chainlit与RAGFlow集成的流式交互应用
功能:接收用户输入,通过RAGFlow API获取流式回答并实时展示
"""
import json
import os
import requests
import datetime
from typing import Dict, Optional
from pprint import pprint
from rocketchat_API.rocketchat import RocketChat
# from rocketchat_API import RocketChatException # 新增异常处理
import chainlit as cl
from chainlit import run_sync
import asyncio
import chainlit as cl
from typing import Optional
import logging
import uuid
from message_queue import RedisQueue
from ragflow_client import RAGFlowClient
# import session_utils
# 配置日志
from logger_config import setup_logger
logger = setup_logger(__name__)
# 初始化Redis队列
redis_queue = RedisQueue()
# 全局消息队列: Chainlit会话ID -> 消息队列
message_queues = {}
message_queues_lock = asyncio.Lock()
# 下面部分的代码很重要,这部分代码的含义是允许所有经过身份验证的用户登录到chainlit
@cl.oauth_callback
def oauth_callback(
provider_id: str,
token: str,
raw_user_data: Dict[str, str],
default_user: cl.User,
) -> Optional[cl.User]:
return default_user
# ==============================================
# 配置参数区域 - 根据实际环境修改以下参数
# ==============================================
# API密钥,从RAGFlow平台获取
API_KEY = os.getenv('RAGFLOW_API_KEY')
# RAGFlow服务基础URL(通常格式为http://ip:port)
BASE_URL = os.getenv('RAGFLOW_BASE_URL')
# 目标AI助手名称(需在RAGFlow平台提前创建)
CHAT_ASSISTANT_NAME = os.getenv('RAGFLOW_ASSISTANT_NAME', 'AI-assist')
# Log OAUTH Keycloak environment variables
logger.debug("【debug】OAUTH_KEYCLOAK_BASE_URL的值为: %s", os.getenv("OAUTH_KEYCLOAK_BASE_URL"))
logger.debug("【debug】OAUTH_KEYCLOAK_REALM的值为: %s", os.getenv("OAUTH_KEYCLOAK_REALM"))
logger.debug("【debug】OAUTH_KEYCLOAK_CLIENT_ID的值为: %s", os.getenv("OAUTH_KEYCLOAK_CLIENT_ID"))
logger.debug("【debug】OAUTH_KEYCLOAK_CLIENT_SECRET的值为: %s", os.getenv("OAUTH_KEYCLOAK_CLIENT_SECRET"))
logger.debug("【debug】OAUTH_KEYCLOAK_NAME的值为: %s", os.getenv("OAUTH_KEYCLOAK_NAME"))
logger.debug("【debug】CHAINLIT_AUTH_SECRET的值为: %s", os.getenv("CHAINLIT_AUTH_SECRET"))
# Log RAGFlow environment variables
logger.debug("【debug】RAGFLOW_API_KEY的值为: %s", os.getenv("RAGFLOW_API_KEY"))
logger.debug("【debug】RAGFLOW_BASE_URL的值为: %s", os.getenv("RAGFLOW_BASE_URL"))
logger.debug("【debug】RAGFLOW_ASSISTANT_NAME的值为: %s", os.getenv("RAGFLOW_ASSISTANT_NAME"))
logger.debug("【debug】IT_ENVIRONMENT的值为: %s", os.getenv("IT_ENVIRONMENT"))
""" @cl.action_callback("转人工")
async def on_action(action: cl.Action):
# print(action.payload)
app_user = cl.user_session.get("user")
# pprint(rocket.me().json())
# pprint(rocket.chat_post_message('good news everyone!', channel='GENERAL', alias='Farnsworth').json())
# Get all the messages in the conversation in the OpenAI format
# print(cl.chat_context.to_openai())
# 将聊天历史格式化为人类易读的文本
# 获取当前星期几(0=周一,6=周日)
current_weekday = datetime.datetime.today().weekday()
# 定义星期几对应的接收者
weekday_users = ['bob', 'david', 'alice', 'tom', 'john', 'jerry', 'jerry']
recipient = weekday_users[current_weekday]
# 将聊天历史格式化为人类易读的文本
formatted_history = '\n\n'.join([f'**{msg["role"].capitalize()}: **{msg["content"]}' for msg in cl.chat_context.to_openai()])
# 在消息前添加用户标识,格式:[USER_ID:xxx]
post_message_response = rocket.chat_post_message(
f"[USER_ID:{app_user.identifier}]{formatted_history}",
room_id=f'@{recipient}',
alias=app_user.identifier
)
print("【debug】post_message_response的值为:", post_message_response.json()) """
@cl.action_callback("转人工")
async def on_action(action: cl.Action):
# 获取当前用户信息
app_user = cl.user_session.get("user")
logger.info(f"【debug】chainlit用户名(邮箱)为: {app_user}")
if not app_user:
logger.error("【debug】未获取到当前用户信息")
await cl.Message(content="无法获取用户信息,转人工失败").send()
return
# 从邮箱提取用户名(与Rocket.Chat用户名匹配)
# username = app_user.identifier.split('@')[0]
username = app_user.identifier
# 将硬编码的Rocket.Chat服务器URL替换为环境变量
server_url = os.getenv("ROCKETCHAT_SERVER_URL")
# 将硬编码的LDAP密码替换为环境变量
password = os.getenv("LDAP_PASSWORD")
chainlit_session_id = cl.user_session.get("id")
try:
# 动态初始化Rocket.Chat客户端
logger.info(f"【debug】用户 {username} 尝试登录Rocket.Chat")
rocket = RocketChat(user=username, password=password, server_url=server_url)
# 验证登录状态
me = rocket.me().json()
if "error" in me:
logger.error(f"【debug】Rocket.Chat登录失败: {me['error']}")
await cl.Message(content="转人工失败:身份验证错误").send()
return
# 获取星期几对应的客服接收者
current_weekday = datetime.datetime.today().weekday()
# Replace hardcoded list
weekday_users = os.getenv("WEEKDAY_USERS", "bob,david,alice,tom,john,jerry,jerry").split(",")
recipient = weekday_users[current_weekday]
logger.info(f"【debug】当前客服接收者: {recipient}")
"""
# 创建或获取与客服的直接聊天频道
logger.info(f"【debug】尝试创建/获取与 {recipient} 的聊天频道")
room_response = rocket.im_create(recipient).json()
# 处理已有频道情况
if "error" in room_response and room_response["error"] == "duplicate-channel":
logger.info(f"【debug】聊天频道已存在,获取现有频道")
rooms = rocket.im_list().json()
room = next((r for r in rooms if recipient in [u['username'] for u in r.get('usernames', []) if u['username'] != username]), None)
if not room:
raise Exception("未找到现有聊天频道")
room_id = room['_id']
elif "error" in room_response:
raise Exception(f"创建频道失败: {room_response['error']}")
else:
room_id = room_response['_id']
logger.info(f"【debug】成功获取聊天频道ID: {room_id}")
"""
# 格式化并发送聊天历史
formatted_history = '\n\n'.join([f'**{msg["role"].capitalize()}: **{msg["content"]}' for msg in cl.chat_context.to_openai()])
message_content = f"[CHAINLIT_USER_ID:{app_user.identifier}]\n{formatted_history}"
# 发送消息到Rocket.Chat
# pprint(rocket.chat_post_message(message_content, channel=f'@{recipient}').json())
post_response = rocket.chat_post_message(
message_content,
channel=f'@{recipient}'
).json()
# logger.info(f"【debug】post_response的值为:, {post_response}")
post_data = post_response
# logger.info(f"【debug】post_data的值为:, {post_data}")
channel_id = post_data.get("message").get("rid")
room_id = channel_id
logger.info(f"【debug】channel_id的值为:, {channel_id}")
logger.info(f"【debug】room_id的值为:, {room_id}")
if post_data.get("success"):
logger.info(f"【debug】聊天历史发送成功,消息ID: {post_data.get('message').get('_id')}, 频道ID: {channel_id}")
await cl.Message(content="已成功转接至人工客服,请等待回复...,若要重新让AI回答,您可以直接新建对话").send()
else:
logger.error(f"【debug】消息发送失败: {post_data}")
await cl.Message(content="转人工失败:消息发送失败").send()
# 保存会话状态
cl.user_session.set("is_human_session", True)
cl.user_session.set("rocket_chat_recipient", recipient)
cl.user_session.set("rocket_chat_room_id", room_id)
cl.user_session.set("rocket_chat_client", rocket)
# 更新Redis会话元数据(存储room_id用于消息路由)
redis_queue.client.hset(
f"chainlit_session:{username}:{chainlit_session_id}:metadata",
mapping={
"room_id": room_id,
"status": "human_chat",
"support_agent": recipient,
"chainlit_session_id": chainlit_session_id,
"用户点击转人工的时间": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
)
# 打印当前所有消息队列状态(调试用)
all_queues = redis_queue.get_all_queues()
# queue_status = {q: redis_queue.qsize(q) for q in all_queues}
queue_status = {q: redis_queue.qsize(q) for q in all_queues}
logger.info(f"【debug】当前消息队列状态: {queue_status}")
logger.info(f"【debug】RedisQueue实例ID: {id(redis_queue)}")
"""
except RocketChatException as e:
logger.error(f"【debug】Rocket.Chat API错误: {str(e)}", exc_info=True)
await cl.Message(content=f"转人工失败: Rocket.Chat通信错误").send()
"""
except Exception as e:
logger.error(f"【debug】转人工处理异常: {str(e)}", exc_info=True)
await cl.Message(content=f"转人工失败: {str(e)}").send()
@cl.on_chat_start
async def on_chat_start():
"""
The on_chat_start decorator is used to define a hook that is called when a new chat session is created.
"""
try:
# 创建并存储RAGFlow客户端实例
logger.info(f"【debug】CHAT_ASSISTANT_NAME的值为: {CHAT_ASSISTANT_NAME}")
rag_client = RAGFlowClient(API_KEY, BASE_URL)
rag_client.get_chat_id(CHAT_ASSISTANT_NAME) # 验证助手是否存在
cl.user_session.set("rag_client", rag_client)
# 初始化会话状态变量
cl.user_session.set("is_human_session", False)
cl.user_session.set("rocket_chat_recipient", None)
app_user = cl.user_session.get("user")
# Replace print statements with logger.debug
# Original: print("【debug】app_user变量的值为:", app_user)
logger.debug(f"app_user变量的值为: {app_user}")
"""
# Original: print("【debug】referenced_docs内容: ", json.dumps(referenced_docs, indent=2, ensure_ascii=False))
logger.debug(f"referenced_docs内容: {json.dumps(referenced_docs, indent=2, ensure_ascii=False)}")
# Original: print("【debug】变量doc_url的值为:", doc_url)
logger.debug(f"变量doc_url的值为: {doc_url}")
# Original: print("【debug】elements变量的值为:", elements)
logger.debug(f"elements变量的值为: {elements}")
"""
"""
# 设置会话映射关系
session_id = cl.user_session.get("id")
# 提取邮箱中的用户名部分作为会话映射键,与Rocket.Chat用户名匹配
rocket_username = app_user.identifier
# logger.info(f"【debug】提取邮箱用户名作为会话键: {rocket_username} (原始邮箱: {app_user.identifier}),会话ID: {session_id}")
session_utils.set_chainlit_session_id(rocket_username, session_id)
logger.info(f"【debug】已设置会话映射: {rocket_username} -> {session_id}")
session_id = cl.user_session.get("id")
async with message_queues_lock:
message_queues[session_id] = asyncio.Queue()
# 打印当前所有消息队列状态
queue_status = {k: v.qsize() for k, v in message_queues.items()}
logger.info(f"【debug】消息队列初始化状态: {queue_status}")
logger.info(f"【debug】为会话 {session_id} 创建消息队列")
"""
# 将会话ID与用户信息存入Redis,便于追踪管理
session_id = cl.user_session.get("id")
user_info = cl.user_session.get("user")
user_id = user_info.identifier if user_info else "anonymous"
redis_queue.client.hset(
f"chainlit_session:{app_user.identifier}:{session_id}:metadata",
mapping={
"user_id": user_id,
"mail": app_user.identifier,
"chainlit_session_id": session_id,
"created_at": datetime.datetime.now().isoformat(),
"status": "active"
}
)
# 设置会话过期时间(24小时)
redis_queue.client.expire(f"chainlit_session:{user_id}:{session_id}:metadata", 86400)
# 启动消息处理任务并保存引用
message_task = asyncio.create_task(process_messages(session_id))
cl.user_session.set("message_task", message_task)
except Exception as e:
await cl.Message(content=f"初始化失败: {str(e)}").send()
""" async def process_messages(session_id: str):
queue_name = f"session:{session_id}"
while True:
try:
# 使用线程池执行同步Redis操作,避免阻塞事件循环
# 增加调试日志并调整超时时间
logger.info(f"【debug】尝试从Redis队列 {queue_name} 读取消息")
message = await asyncio.to_thread(
redis_queue.dequeue,
queue_name,
block=True,
timeout=30 # 延长超时时间以确保能接收到消息
)
if message is not None:
logger.info(f"【debug】从队列 {queue_name} 读取到消息: {message[:50]}")
else:
logger.info(f"【debug】队列 {queue_name} 超时未读取到消息")
if message is None:
continue
# 处理消息并发送到Chainlit
await cl.Message(content=message).send()
except asyncio.CancelledError:
logger.info(f"【debug】会话 {session_id} 消息处理任务已取消")
break
except Exception as e:
logger.error(f"Error processing message for session {session_id}: {str(e)}", exc_info=True)
break
"""
async def process_messages(session_id: str):
"""
这个函数主要负责从Redis队列中读取消息并发送到Chainlit。
参数session_id指的是chainlit中的会话ID。
它会根据会话ID动态获取对应的消息队列名称,并在队列中读取消息。
如果读取到消息,会将其发送到Chainlit进行展示;如果读取超时或队列空,会进行适当的日志记录。
这个函数被on_chat_start调用。
"""
while True:
try:
# 动态获取当前room_id(支持转人工后更新)
# current_room_id = redis_queue.client.hget(metadata_key, "room_id") or room_id
# if current_room_id != room_id:
# logger.info(f"【debug】房间ID变更: {room_id} -> {current_room_id}")
# room_id = current_room_id
# queue_name = f"session:{room_id}:metadata"
# if not room_id:
# # 未进入人工会话,使用默认队列
# queue_name = f"session:{session_id}"
# logger.info(f"【debug】未进入人工会话,使用默认队列: {queue_name}")
# await asyncio.sleep(5) # 降低空轮询频率
# continue
# 从Redis获取room_id
metadata_queue_name = f"chainlit_session:{cl.user_session.get("user").identifier}:{session_id}:metadata"
# logger.info(f"【debug】尝试从Redis获取会话元数据: {metadata_queue_name}")
metadata = redis_queue.client.hgetall(metadata_queue_name)
# logger.info(f"【debug】获取到的会话元数据: {metadata}")
# Get room_id with proper error handling
# room_id = metadata.get("room_id")
# logger.info(f"【debug】从会话元数据中获取room_id: {room_id}")
room_id = cl.user_session.get("rocket_chat_room_id")
logger.info(f"【debug】从用户会话中获取rocket_chat_room_id: {room_id}")
if room_id is None:
logger.warning(f"【警告】会话 {session_id} 的元数据中未找到 room_id这个key,说明此用户没有选择人工客服。使用默认值")
room_id = "不存在room_id,此用户没有选择人工客服"
logger.info(f"【debug】消息处理初始化 - 会话ID: {session_id}, 房间ID: {room_id}")
if room_id == "不存在room_id,此用户没有选择人工客服":
# 未进入人工会话,使用默认队列
logger.info(f"【debug】此用户 {cl.user_session.get("user").identifier}---{session_id} 没有选择人工客服,未进入人工会话,不读取消息")
await asyncio.sleep(5) # 降低空轮询频率
continue
else:
queue_name = f"{os.getenv("IT_ENVIRONMENT")}:rocket.chat_session:{cl.user_session.get("rocket_chat_recipient")}:{room_id}:messages_queue"
logging.info(f"【debug】此用户{cl.user_session.get("user").identifier}--- {session_id} 选择了人工客服,使用队列: {queue_name}")
logger.info(f"【debug】尝试从Redis队列 {queue_name} 读取消息")
# message = await asyncio.to_thread(
# redis_queue.dequeue,
# queue_name,
# block=True,
# timeout=30
# )
data = await asyncio.to_thread(
redis_queue.stream_peek_latest,
queue_name,
block=True,
timeout=30
)
if data is None:
logger.info(f"【debug】队列 {queue_name} 超时未读取到消息")
continue
else:
message = data['data']['data']
logger.info(f"【debug】从队列 {queue_name} 读取到消息: {message}")
await cl.Message(content=message).send()
logger.info(f"【debug】消息已发送到Chainlit用户:{cl.user_session.get("user").identifier},人工客服:{cl.user_session.get("rocket_chat_recipient")} 房间ID为:{room_id},消息内容为:{message}")
except asyncio.CancelledError:
logger.info(f"【debug】会话 {session_id} 消息处理任务已取消")
break
except Exception as e:
logger.error(f"Error processing message for session {session_id}: {str(e)}", exc_info=True)
break
@cl.set_starters
async def set_starters():
return [
cl.Starter(
label="AI助手使用手册",
message="AI助手使用手册.",
icon="http://10.64.160.146/arrow-right-circle-line.svg",
),
cl.Starter(
label="如何重置密码",
message="如何重置密码.",
icon="http://10.64.160.146/arrow-right-circle-line.svg",
),
cl.Starter(
label="公司食堂开餐时间",
message="公司食堂开餐时间.",
icon="http://10.64.160.146/arrow-right-circle-line.svg",
),
cl.Starter(
label="怎么查打卡时间",
message="怎么查打卡时间.",
icon="http://10.64.160.146/arrow-right-circle-line.svg",
)
]
@cl.on_message
async def on_message(message: cl.Message):
"""
The on_message decorator is used to define a hook that is called when a new message is received from the user.
这个decorator主要是用来决定chainlit用户发送的消息是要发送给rocke.chat人工客服还是后端的ragflow
"""
rag_client: RAGFlowClient = cl.user_session.get("rag_client")
is_human_session = cl.user_session.get("is_human_session", False)
rocket_chat_recipient = cl.user_session.get("rocket_chat_recipient")
if not rag_client:
await cl.Message(content="客户端未初始化,请刷新页面重试").send()
return
try:
if is_human_session and rocket_chat_recipient:
# 人工会话,直接发送到Rocket.Chat
app_user = cl.user_session.get("user")
# 添加用户标识和人工会话标记
formatted_message = f"[CHAINLIT_USER_ID:{app_user.identifier}][HUMAN_SESSION]\n{message.content}"
# Retrieve Rocket.Chat client from user session
current_rocket = cl.user_session.get("rocket_chat_client")
logger.info(f"【debug】当前Rocket.Chat客户端: {current_rocket}")
if not current_rocket:
logger.error("【debug】Rocket.Chat client not found in user session")
await cl.Message(content="转人工会话已过期,请重新发起转人工请求").send()
return
logger.info(f"【debug】发送消息到Rocket.Chat房间(客服名称): {rocket_chat_recipient}")
current_rocket.chat_post_message(
formatted_message,
room_id=f'@{rocket_chat_recipient}',
)
# 向用户确认消息已发送给人工代理
await cl.Message(content=f"已发送给人工客服:{rocket_chat_recipient}\n消息内容为: {message.content}").send()
logger.info(f"【debug】人工会话消息发送: {formatted_message}")
else:
# AI会话,使用RAGFlow处理
msg = await cl.Message(content="").send()
user_identifier = cl.user_session.get("user").identifier
await rag_client.stream_chat_completion(message.content, msg)
except Exception as e:
error_msg = f"处理请求时出错: {str(e)}"
await cl.Message(content=error_msg).send()
logger.error(f"【debug】消息处理错误: {error_msg}", exc_info=True)
@cl.on_chat_end
async def on_chat_end():
chainlit_session_id = cl.user_session.get("id")
if not chainlit_session_id:
return
# 获取消息处理任务并取消
message_task = cl.user_session.get("message_task")
if message_task:
message_task.cancel()
try:
await message_task
except asyncio.CancelledError:
pass
# 清理Redis队列
queue_name = f"chainlit_session:{cl.user_session.get("user").identifier}:{chainlit_session_id}:metadata"
redis_queue.clear(queue_name)
logger.info(f"chainlit Session {chainlit_session_id} ended. Redis queue {queue_name} cleared.")
else:
logger.info(f"【debug】会话 {chainlit_session_id} 结束,消息队列不存在")
实现data persistent(暂未实现)
后端–知识库引擎ragflow
参考资料:RAGFlow | RAGFlow
规划
将ragflow作为后端,专门处理rag任务
如何部署和配置ragflow
详细步骤请查看此文:《ragflow》
代码
主要是ragflow_client.py
import os
import requests
import aiohttp
import json
from typing import Optional
import chainlit as cl
from logger_config import setup_logger
logger = setup_logger(__name__)
CHAT_ASSISTANT_NAME = os.getenv('RAGFLOW_ASSISTANT_NAME', 'AI-assist')
class RAGFlowClient:
"""RAGFlow API客户端,处理与后端的交互"""
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
self.chat_id: Optional[str] = None
def get_chat_id(self, assistant_name: str) -> str:
"""根据助手名称获取聊天ID"""
url = f"{self.base_url}/api/v1/chats?name={assistant_name}"
response = requests.get(url, headers=self.headers)
response_data = response.json()
if response_data['code'] != 0:
raise ValueError(f"获取助手ID失败: {response_data['message']}")
if not response_data['data']:
raise ValueError(f"未找到名称为'{assistant_name}'的AI助手")
return response_data['data'][0]['id']
def create_chat_session(self) -> str:
"""创建新的聊天会话"""
if not self.chat_id:
self.chat_id = self.get_chat_id(CHAT_ASSISTANT_NAME)
url = f"{self.base_url}/api/v1/chats/{self.chat_id}/sessions"
session_name = cl.user_session.get("user").identifier
response = requests.post(
url, headers=self.headers, json={'name': f"chaint_session:{session_name}"}
)
response_data = response.json()
if response_data['code'] != 0:
raise ValueError(f"创建会话失败: {response_data['message']}")
return response_data['data']['id']
async def stream_chat_completion(self, question: str, msg: cl.Message):
"""流式获取聊天完成结果并直接发送到Chainlit前端"""
session_id = self.create_chat_session()
url = f"{self.base_url}/api/v1/chats/{self.chat_id}/completions"
payload = {
"question": question,
"stream": True,
"session_id": session_id,
"user_id": cl.user_session.get("user").identifier
}
# 用于跟踪已发送的内容,避免重复
sent_content = ""
# 存储引用的文档
referenced_docs = []
with requests.post(
url, headers=self.headers, json=payload, stream=True
) as response:
for line in response.iter_lines():
if line:
try:
# 处理SSE格式数据
line = line.decode('utf-8').lstrip('data: ').strip()
if not line:
continue
json_data = json.loads(line)
# 检查是否为结束标志
if json_data.get('code') == 0 and json_data.get('data') is True:
break
if (
isinstance(json_data, dict) and
json_data.get('code') == 0 and
isinstance(json_data.get('data'), dict)
):
answer_chunk = json_data['data'].get('answer', '')
# 提取引用文档信息(使用doc_aggs聚合数据避免重复)
if 'reference' in json_data['data']:
reference_data = json_data['data']['reference']
# 优先使用doc_aggs获取聚合的引用文档信息
if 'doc_aggs' in reference_data and isinstance(reference_data['doc_aggs'], list):
# 遍历聚合文档列表
for doc_info in reference_data['doc_aggs']:
doc_id = doc_info.get('doc_id')
doc_name = doc_info.get('doc_name')
# 检查文档ID和名称是否存在
if doc_id and doc_name:
referenced_docs.append((doc_id, doc_name))
# 兼容处理:如果没有doc_aggs则使用chunks(旧版API兼容)
elif 'chunks' in reference_data:
seen_document_ids = set()
for chunk in reference_data['chunks']:
doc_id = chunk.get('document_id')
doc_name = chunk.get('document_name')
if doc_id and doc_name and doc_id not in seen_document_ids:
referenced_docs.append((doc_id, doc_name))
seen_document_ids.add(doc_id)
if answer_chunk and answer_chunk != sent_content:
# 计算新增内容
new_content = answer_chunk[len(sent_content):]
if new_content:
await msg.stream_token(new_content)
sent_content = answer_chunk
except json.JSONDecodeError:
continue
# 使用Chainlit File元素展示引用文档
if referenced_docs:
# 调试:打印引用文档列表所有内容
# Replace print statements with logger.debug
# Original: print("【debug】referenced_docs内容: ", json.dumps(referenced_docs, indent=2, ensure_ascii=False))
# logger.debug(f"referenced_docs内容: {json.dumps(referenced_docs, indent=2, ensure_ascii=False)}")
logger.debug(f"referenced_docs内容: {referenced_docs}")
elements = []
for doc_id, doc_name in referenced_docs:
doc_url = f"{self.base_url}/document/{doc_id}?ext={doc_name.split('.')[-1]}&prefix=document"
logger.debug(f"变量doc_url的值为: {doc_url}")
elements.append(cl.File(name=doc_name, url=doc_url))
# 打印elements变量的值到终端,方便诊断
logger.debug(f"elements变量的值为: {elements}")
actions = [
cl.Action(
name="转人工",
icon="mouse-pointer-click",
payload={"value": "example_value"},
label="点我立即转人工"
)
]
# 发送包含文件元素的消息
await cl.Message(content="\n\n### 引用文档", elements=elements, actions=actions).send()
# 明确标记消息完成状态
await msg.update()
fastapi–用于接收rocket.chat发送过来的数据
安装fastapi
pip install -U fastapi uvicorn
代码
主要是webhook_server.py
from fastapi import FastAPI, Request, HTTPException
import uvicorn
import json
import os
import chainlit as cl
from chainlit.utils import mount_chainlit
# import session_utils
# from chainlit_ragflow_streaming import message_queues, message_queues_lock
from message_queue import RedisQueue
from pydantic import BaseModel
class MessageRequest(BaseModel):
session_id: str
message: str
# 配置日志
# Replace current logging setup with:
from logger_config import setup_logger
logger = setup_logger(__name__)
app = FastAPI()
redis_queue = RedisQueue()
# 存储会话映射: Rocket.Chat用户ID -> Chainlit会话ID
session_mapping = {}
@app.post("/rocketchat-webhook")
async def rocketchat_webhook(request: Request):
"""
处理Rocket.Chat发送过来的webhook消息。
:param request: 包含Rocket.Chat消息的请求对象
:type request: Request
:return: 处理结果
:rtype: dict
"""
try:
data = await request.json()
except JSONDecodeError:
logger.error("Invalid JSON format in request")
raise HTTPException(status_code=400, detail="Invalid JSON format")
except KeyError as e:
logger.error(f"Missing required field: {str(e)}")
raise HTTPException(status_code=400, detail=f"Missing required field: {str(e)}")
except redis.ConnectionError:
logger.error("Redis connection failed")
raise HTTPException(status_code=503, detail="Service temporarily unavailable")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="An unexpected error occurred")
logger.info(f"【debug】收到Rocket.Chat webhook: {json.dumps(data, ensure_ascii=False)}")
# 验证webhook令牌(从body中获取token)
token = data.get("token")
expected_token = os.getenv("ROCKETCHAT_WEBHOOK_TOKEN")
if not token:
logger.warning("【debug】Webhook请求中未找到token")
raise HTTPException(status_code=400, detail="Token not found in request body")
if token != expected_token:
logger.warning(f"【debug】Webhook令牌验证失败: 收到{token}, 预期{expected_token}")
raise HTTPException(status_code=403, detail="Invalid token")
# 提取消息内容和发送者(根据实际JSON结构调整)
content = data.get("text", "")
sender_username = data.get("user_name", "")
room_id = data.get("channel_id", "")
message_id = data.get("message_id", "")
timestamp = data.get("timestamp", "")
# 添加调试日志
logger.info(f"【debug】提取消息: sender(Support agent)={sender_username}, content={content[:50]}, room_id={room_id}, message_id={message_id}, timestamp={timestamp}")
# 验证Redis连接并检查队列
try:
# 检查Redis连接
redis_queue.client.ping()
logger.info("【debug】Redis连接正常")
# 打印Redis实例基本信息
logger.debug(f"【debug】Redis连接信息 - IP: {redis_queue.host}, 端口: {redis_queue.port}, 数据库索引: {redis_queue.db}")
except Exception as e:
logger.error(f"【debug】Redis连接失败: {str(e)}")
raise HTTPException(status_code=500, detail="Redis connection failed")
# 过滤掉系统消息和自动转发的消息
if "[CHAINLIT_USER_ID:" in content or "[HUMAN_SESSION]" in content:
logger.info(f"【debug】过滤掉自动转发的消息: {content}")
return {"status": "ignored"}
agent_reply = f"**人工客服 {sender_username}:\n {content}"
# 消息队列名称
queue_name = f"{os.getenv("IT_ENVIRONMENT")}:rocket.chat_session:{sender_username}:{room_id}:messages_queue"
# 入队前检查队列是否存在
# queue_type = redis_queue.client.type(queue_name)
# if queue_type != 'list':
# logger.warning(f"【debug】队列 {queue_name} 不是列表类型,实际类型: {queue_type}")
# raise HTTPException(status_code=500, detail=f"Queue {queue_name} is not a list type")
if redis_queue.qsize(queue_name) == 0 and queue_name not in redis_queue.get_all_queues():
logger.warning(f"【debug】队列 {queue_name} 不存在,将创建新队列")
# 消息入队,同时设置队列仅保留最近1000条消息,避免内存溢出
redis_queue.enqueue_stream(queue_name, agent_reply)
# 入队后验证
new_size = redis_queue.qsize(queue_name)
logger.info(f"【debug】消息入队成功,队列 {queue_name} 当前大小: {new_size}, 消息内容: {agent_reply}")
return {"status": "success"}
# logger.info(f"【debug】消息入队成功,队列名称: {queue_name} 当前大小: {redis_queue.qsize(queue_name)}")
"""
# 查询会话映射前添加详细日志
logger.info(f"【debug】准备查询会话映射: sender_username={sender_username}")
# chainlit_session_id = session_utils.get_chainlit_session_id(sender_username)
# all_mappings = session_utils.get_all_session_mappings()
# 从Redis的session_mapping哈希表中获取会话ID
chainlit_session_id = redis_queue.client.hget("session_mapping", sender_username)
# 获取所有会话映射
all_mappings = redis_queue.client.hgetall("session_mapping")
logger.info(f"【debug】查询会话映射结果: username={sender_username}, session_id={chainlit_session_id}, 当前映射={all_mappings}")
# 获取并记录当前所有会话映射状态
logger.info(f"【debug】当前会话映射: {all_mappings}")
"""
"""
if not chainlit_session_id:
# 打印当前所有消息队列状态以辅助调试
# queue_status = {k: v.qsize() for k, v in message_queues.items()}
queue_status = {q: redis_queue.qsize(q) for q in redis_queue.get_all_queues()}
logger.info(f"【debug】未找到会话,当前消息队列状态: {queue_status}")
logger.warning(f"【debug】未找到对应的Chainlit会话: {sender_username}")
return {"status": "session_not_found"}
"""
"""
queue_name = f"session:{chainlit_session_id}"
logger.info(f"【debug】准备入队消息到队列 {queue_name}, 会话ID: {chainlit_session_id}")
"""
# 挂载Chainlit应用
mount_chainlit(app=app, target="chainlit_ragflow_streaming.py", path="/")
"""
@app.post("/send-message")
async def send_message(data: MessageRequest):
# 接收Rocket.Chat的webhook消息,处理并入队。
# :param data: 包含会话ID和消息内容的请求体
# :type data: MessageRequest
# :return: 入队状态
# :rtype: dict
try:
session_id = data.session_id
message = data.message
# 打印当前所有消息队列状态(调试用)
all_queues = redis_queue.get_all_queues()
# queue_status = {q: redis_queue.qsize(q) for q in all_queues}
queue_status = {q: redis_queue.qsize(q) for q in redis_queue.get_all_queues()}
logger.info(f"【debug】当前消息队列状态: {queue_status}")
logger.info(f"【debug】RedisQueue实例ID: {id(redis_queue)}")
# 检查会话队列(消息队列:真正存放消息的队列)是否存在
queue_name = f"session:{session_id}"
if redis_queue.qsize(queue_name) == 0 and queue_name not in all_queues:
logger.warning(f"会话 {session_id} 的消息队列不存在")
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
# 将消息放入对应会话的队列
redis_queue.enqueue(queue_name, message)
logger.info(f"【debug】消息已放入会话 {session_id} 的队列,当前队列大小: {redis_queue.qsize(queue_name)}")
return {"status": "success", "message": "Message added to queue"}
except Exception as e:
logger.error(f"处理消息时出错: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Error processing message: {str(e)}")
"""
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
redis消息队列
安装redis
关于redis的介绍和详细安装教程,请查看:《redis》
安装redis模块
pip install -U redis
有关核心代码
主要是message_queue.py
import os
import redis
import time
from typing import Optional, Any
# Initialize logger for RedisQueue
from logger_config import setup_logger
logger = setup_logger(__name__)
class RedisQueue:
def __init__(self, host: str = None, port: int = None, db: int = None, password: str = None):
self.host = host or os.getenv("REDIS_HOST", "localhost")
self.port = port or int(os.getenv("REDIS_PORT", 6379))
# Get environment and set appropriate Redis DB index
environment = os.getenv("IT_ENVIRONMENT", "dev")
if environment == "dev":
self.db = int(os.getenv("REDIS_DB_INDEX_DEV", 0))
elif environment == "test":
self.db = int(os.getenv("REDIS_DB_INDEX_TEST", 1))
else:
self.db = 0 # Default to dev DB if environment not specified
logger.warning(f"Unknown environment {environment}, defaulting to development database (index 0)")
self.password = password or os.getenv("REDIS_PASSWORD")
# Load database index from environment configuration
self.client = redis.Redis(
host=self.host,
port=self.port,
db=self.db,
password=self.password,
decode_responses=True
)
try:
self.client.ping()
except redis.ConnectionError:
raise Exception("Could not connect to Redis server. Please ensure Redis is running.")
# async def __aenter__(self):
# self.client = await aioredis.from_url(self.connection_string)
# return self
# async def __aexit__(self, exc_type, exc, tb):
# await self.client.close()
def enqueue(self, queue_name: str, item: Any) -> int:
"""Add an item to the end of the queue"""
return self.client.rpush(queue_name, item)
def dequeue(self, queue_name: str, block: bool = True, timeout: int = 0) -> Optional[Any]:
"""Remove and return an item from the front of the queue"""
if block:
# Blocking mode: waits for item with timeout
item = self.client.blpop(queue_name, timeout=timeout)
logger.debug(f"Blocking dequeue from {queue_name}: {item}")
return item[1] if item else None
else:
# Non-blocking mode: returns immediately
logger.debug(f"Non-blocking dequeue from {queue_name}: {item}")
return self.client.lpop(queue_name)
def enqueue_stream(self, stream_name: str, item: any, maxlen: int = 1000) -> str:
"""
Add an item to a Redis Stream with automatic trimming to avoid memory overuse.
Args:
stream_name: Name of the stream (queue name).
item: A dictionary representing the fields of the message.
maxlen: Maximum number of messages to retain in the stream.
Returns:
The ID of the added message.
"""
try:
return self.client.xadd(
stream_name,
fields={"data": str(item)},
maxlen=maxlen,
approximate=True
)
except redis.RedisError as e:
logger.error(f"Failed to enqueue to stream '{stream_name}': {e}")
return ""
def stream_peek_latest(self, stream_name: str, block: bool = True, timeout: int = 0) -> Optional[dict]:
"""
Blocking or non-blocking peek of the latest item from a Redis Stream without deleting it.
Mimics the behavior of dequeue but doesn't remove the item.
Args:
stream_name: Name of the Redis stream (acts like a queue).
block: Whether to block until a new item is available.
timeout: Maximum blocking time in seconds (only if block=True).
Returns:
A dict with 'id' and 'data' keys if message exists, else None.
"""
try:
# Always read from the latest known ID to avoid re-reading
last_id = "$" # "$" = last inserted ID, means "only read new items after now"
block_ms = int(timeout * 1000) if block else 0
response = self.client.xread({stream_name: last_id}, count=1, block=block_ms)
if not response:
return None
_, messages = response[0]
message_id, message_data = messages[0]
return {"id": message_id, "data": message_data}
except redis.RedisError as e:
logger.error(f"Failed to peek latest from stream '{stream_name}': {e}")
return None
def qsize(self, queue_name: str) -> int:
"""
Return the size of the queue, supporting both List and Stream types"
"""
try:
# Get the type of the Redis key
key_type = self.client.type(queue_name)
if key_type == 'list':
return self.client.llen(queue_name)
elif key_type == 'stream':
return self.client.xlen(queue_name)
else:
logger.warning(f"Unsupported queue type: {key_type} for queue {queue_name}")
return 0
except redis.RedisError as e:
logger.error(f"Failed to get queue size for {queue_name}: {e}")
return 0
def clear(self, queue_name: str) -> int:
"""Clear all items from the queue"""
return self.client.delete(queue_name)
def get_all_queues(self, prefix: str = "session:") -> list:
"""Get all queue names with the given prefix that are lists"""
keys = self.client.keys(f"{prefix}*")
list_keys = []
for key in keys:
# 只返回列表类型的键
if self.client.type(key) == 'list':
list_keys.append(key)
return list_keys
如何部署redis
rocket.chat
详细的rocket.chat部署和配置教程请查看:《rocket.chat–开源的跨平台即时通讯软件》
安装rocket.chat Python SDK
关于rocket.chat Python SDK详细介绍请看此文:《rocket.chat python api wrapper示例》
pip install -U rocketchat_API
关于outgoing webhook的特别说明
事件触发后,rocket.chat会立即向目标url发生器默认的数据结构的数据(不是必须要启用script),但我在官网没有看到详细说明,为了搞清楚,其默认发送的数据结构是怎样的,可以做个简单的实验。
利用https://webhook.site/ 可以非常便捷的创建webhook目标url(打开网站后,会自动创建url),然后将生成的url填入rocket.chat outgoing webhook的url栏中。
下图红框中的url就是网站自动生成的,单击即可自动复制到剪切板
在rocket.chat的outgoing webhook中,创建一个新的outgoing webhook,最关键的是url,直接粘贴即可
script可以不用启用
现在向任何一个rocket.chat用户发送一条消息,观察webhook.site网站是否接受到了数据
可以看到,网站立即接受到了数据,可以查看详细的数据结构
text字段的值就是刚刚用rocket.chat发送的消息,这个一般是我们最关注的。需要注意的是,如果消息中包含了附件,如视频,图片等,则json格式可能会跟下面的不同,最好再测试下。
知道了数据结构,就知道要怎么解析rocket.chat发送的数据了。
{
"token": ":r1qf:",
"bot": false,
"channel_id": "LhgrHveTi5eW4bBwcScQxnLPQs6Y7SqJ9R",
"message_id": "rZkpM2rjmya56phSw",
"timestamp": "2025-06-21T11:15:14.001Z",
"user_id": "ScQxnLPQs6Y7SqJ9R",
"user_name": "jerry",
"text": "hello webhook site",
"siteUrl": "http://10.65.37.237:3000"
}
日志处理
from doctest import debug
import logging
import sys
import os
from typing import Dict, Any
def setup_logger(name: str = __name__, level: int = logging.INFO) -> logging.Logger:
"""
Set up standardized logger with both file and console handlers
Args:
name: Logger name
level: Logging level (default: INFO)
Returns:
Configured logger instance
"""
# Create or get logger
logger = logging.getLogger(name)
# Avoid reconfiguring existing logger
if logger.handlers:
return logger
# Set level based on environment or configuration
log_level = os.getenv('LOG_LEVEL', 'DEBUG').upper()
logger.setLevel(getattr(logging, log_level, logging.INFO))
# Make propagation configurable
propagate_logs = os.getenv('PROPAGATE_LOGS', 'False').lower() == 'true'
logger.propagate = propagate_logs
# Add handlers (example for file and console)
# [Handler configuration would go here]
# Create formatters
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_formatter = logging.Formatter(
'%(levelname)s - %(message)s'
)
# Create file handler with UTF-8 encoding
file_handler = logging.FileHandler('app.log', encoding='utf-8')
file_handler.setFormatter(file_formatter)
# Create console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(console_formatter)
# Add handlers to logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
环境变量(.env文件)
为了方便对程序进行调整,我创建了.env文件,这个文件你需要自行创建,根据你自己实际情况修改,以下是我的.env文件示例:
# API Keys 注意:本项目未使用到下面的api key
#DASHSCOPE_API_KEY=
#OPENAI_API_KEY=
# RAGFlow Configuration
RAGFLOW_API_KEY=ragflow-c3NmJlODE4NDc2NTExZjBiNGQ0MDY0Mz
RAGFLOW_BASE_URL=http://10.65.37.238
RAGFLOW_ASSISTANT_NAME=AI-assist
# RocketChat Configuration
# ROCKETCHAT_USER_ID=LhgrHveTi5eW4bBwc
# ROCKETCHAT_AUTH_TOKEN=sj7to84Zr1st8jefMquQfOBfOgQRNHQQRlwbz8bkmIg
ROCKETCHAT_SERVER_URL=http://10.65.37.237:3000
ROCKETCHAT_WEBHOOK_TOKEN=test-token
# 添加LDAP密码配置
LDAP_PASSWORD=1
# 指定rocket.chat中的人工客服用户名及排班(周一到周日)
WEEKDAY_USERS=bob,david,alice,tom,john,jerry,jerry
# OAuth Configuration (Keycloak)
OAUTH_KEYCLOAK_BASE_URL=http://10.65.37.239:8080
OAUTH_KEYCLOAK_REALM=master
OAUTH_KEYCLOAK_CLIENT_ID=chainlit-test-my
OAUTH_KEYCLOAK_CLIENT_SECRET=uuIlgNDCHbvKy0MNGXlSeOcxK3YtNVyf
OAUTH_KEYCLOAK_NAME=chainlit-keycloak
CHAINLIT_AUTH_SECRET=uuIlgNDCHbvKy0MNGXlSeOcxK3YtNVyf
# Redis Configuration
# IT_ENVIRONMENT这个环境变量主要是定义程序运行在何种环境下,可以填dev、test、production、demo等等,分别表示开发环境、测试环境、生产环境和演示环境
#REDIS_DB_INDEX_DEV和REDIS_DB_INDEX_TEST用于定义REDIS中的数据库索引,
#由于redis官方不建议通过数据库索引来区分环境,所以这里我们通过环境变量来区分环境,所有队列仍然在同一个数据库索引(即db 0)里面
IT_ENVIRONMENT=dev
REDIS_DB_INDEX_DEV=0
REDIS_DB_INDEX_TEST=0
# Redis Configuration
REDIS_HOST=10.65.37.237
REDIS_PORT=6379
REDIS_PASSWORD=<此处替换为你的redis密码>
# Logging Configuration
# Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOG_LEVEL=DEBUG
# Enable/disable log propagation to parent loggers
PROPAGATE_LOGS=False
验证
简单提问
点击上图的文件后,即可在线浏览原始文档,便于追溯,也方便用户查看详细的原始文档。
验证直接用英语提问,是否能正确回答:
从下图可知,虽然我没有在ragflow的知识库中存入英文版的文档,ragflow仍然能自动给出合适的英文回答,这对于外籍员工十分有用。
测试转人工功能
同一个用户在不同电脑上登录了chainlit并且点击了“转人工”
测试过,如果同一个用户,在不同电脑上登录了chainlit,并且都点击了“转人工”按钮,则后续,用户在哪个电脑上发送消息,rocket.chat人工客服回复的消息就会出现在哪个电脑的chainlit UI上。
故障诊断
KeyError:‘chunk_id’
与ragflow中的AI助理对话时,每次对话结束后,都会出现这个报错,我还将此问题反馈到了github上,issue链接为:[Bug]: KeyError: ‘chunk_id’ when streaming responses via ragflow-sdk session.ask() · Issue #7011 · infiniflow/ragflow
后来ragflow维护者答复我了,我反馈的问题是个bug,虽然他也告诉我解决方案了,但是我感觉比较麻烦,最终我还是没有使用ragflow python sdk,而是直接用的ragflow HTTP api将chainlit与ragflow集成。
后来我查阅了ragflow官方的http API文档,发现了这样一段提示,如下图,意思是说,在streaming模式下,最后一条消息是空消息,这让我发现了问题所在,因为最后一次响应中并没有chunk_id这个字段,所以报错了,所以我重修修改了代码,最终解决了此问题
以下是完整的python代码,最关键的是第36行后面的代码,其中的这三行是关键:
`# 检查是否为结束消息`
`elif isinstance(ans, dict) and ans.get('code') == 0 and ans.get('data') is True:`
`break`
# Import RAGFlow SDK
from ragflow_sdk import RAGFlow
from chainlit.oauth_providers import OAuthProvider
import chainlit as cl
import httpx
from fastapi import HTTPException
from chainlit.user import User
# Initialize RAGFlow object
# api_key: Authentication API key
# base_url: Base URL of RAGFlow service
rag_object = RAGFlow(
api_key="ragflow-c3NmJlODE4NDc2NTExZjBiNGQ0MDY0Mz",
base_url="http://10.65.37.238"
)
# Get specific assistant by name
assistant = rag_object.list_chats(name="AI-assist")[0]
# List all available assistants (for debugging)
""" for assistant in rag_object.list_chats():
print(assistant) """
# Create new chat session
session = assistant.create_session()
# Print welcome message
print("\n==================== test-AI-assist =====================\n")
print("Hello. What can I do for you?")
# 在创建会话时设置stream=False
""" user_question = "如何重置OA密码?"
chat_assisitant_output = session.ask(question=user_question, stream=False)
print(chat_assisitant_output) """
# Main conversation loop
while True:
# Get user input
question = input("\nPlease question:\n> ")
# question = "如何重置OA密码?"
# Stream assistant's response
# 初始化变量存储完整响应
full_response = ""
cont = ""
print("\n==================== test-AI-assist =====================\n")
try:
# 处理流式响应
for ans in session.ask(question=question, stream=True):
# 检查是否为有效的Message对象且包含content
if hasattr(ans, 'content') and ans.content:
# 计算新增内容
new_content = ans.content[len(full_response):]
# 打印新增内容
print(new_content, end='', flush=True)
# 更新完整响应
full_response = ans.content
# 检查是否为结束消息
elif isinstance(ans, dict) and ans.get('code') == 0 and ans.get('data') is True:
break
print() # 添加换行
except KeyError as e:
# 忽略chunk_id相关的KeyError,不输出错误信息
if 'chunk_id' not in str(e):
print(f"\nUnexpected error: {e}")
except Exception as e:
print(f"\nUnexpected error: {e}")
finally:
print("\n\n==================== test-AI-assist =====================\n")
输出如下:
Chainlit context not found
完整报错如下:
出现这个问题的原因是,只能在chainlit上下文中调用chainlit中的函数,而上面的上面试图在fastapi中直接调用chainlit中的函数向chainlit UI发送消息,这是不行的。
这个问题困扰了好几天,最终通过redis解决了,详情可以查看本项目的源代码和项目总结报告