diff --git a/cmd/server/main.go b/cmd/server/main.go index ff210cd..9502953 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -31,7 +31,7 @@ func main() { gin.SetMode(gin.ReleaseMode) } else { gin.SetMode(gin.DebugMode) - config.SetEnv() + //config.SetEnv() } // Build dependency injection container diff --git a/config/bak b/config/bak new file mode 100644 index 0000000..4c04102 --- /dev/null +++ b/config/bak @@ -0,0 +1,536 @@ +# 服务器配置 +server: + port: 8088 + host: "0.0.0.0" + +# 对话服务配置 +conversation: + max_rounds: 5 + keyword_threshold: 0.3 + embedding_top_k: 10 + vector_threshold: 0.5 + rerank_threshold: 0.7 + rerank_top_k: 5 + fallback_strategy: "fixed" + fallback_response: "抱歉,我无法回答这个问题。" + fallback_prompt: | + 你是一个专业、友好的AI助手。现在用户提出的问题超出了你的知识库范围,你需要生成一个礼貌且有帮助的回复。 + + ## 回复要求 + - 诚实承认你无法提供准确答案 + - 简洁友好,不要过度道歉 + - 可以提供相关的建议或替代方案 + - 回复控制在50字以内 + - 使用礼貌、专业的语气 + + ## Few-shot示例 + + 用户问题: 今天杭州西湖的游客数量是多少? + 回复: 抱歉,我无法获取实时的杭州西湖游客数据。您可以通过杭州旅游官网或相关APP查询这一信息。 + + 用户问题: 张教授的新论文发表了吗? + 回复: 我没有张教授的最新论文信息。建议您查询学术数据库或直接联系张教授获取最新动态。 + + 用户问题: 我的银行卡号是多少? + 回复: 作为AI助手,我无法获取您的个人银行信息。请登录您的银行APP或联系银行客服获取相关信息。 + + ## 用户当前的问题是: + {{.Query}} + enable_rewrite: true + enable_rerank: true + rewrite_prompt_system: | + 你是一个专注于指代消解和省略补全的智能助手,你的任务是根据历史对话上下文,清晰识别用户问题中的代词并替换为明确的主语,同时补全省略的关键信息。 + + ## 改写目标 + 请根据历史对话,对当前用户问题进行改写,目标是: + - 进行指代消解,将"它"、"这个"、"那个"、"他"、"她"、"它们"、"他们"、"她们"等代词替换为明确的主语 + - 补全省略的关键信息,确保问题语义完整 + - 保持问题的原始含义和表达方式不变 + - 改写后必须也是一个问题 + - 改写后的问题字数控制在30字以内 + - 仅输出改写后的问题,不要输出任何解释,更不要尝试回答该问题,后面有其他助手回去解答此问题 + + ## Few-shot示例 + + 示例1: + 历史对话: + 用户: 微信支付有哪些功能? + 助手: 微信支付的主要功能包括转账、付款码、收款、信用卡还款等多种支付服务。 + + 用户问题: 它的安全性 + 改写后: 微信支付的安全性 + + 示例2: + 历史对话: + 用户: 苹果手机电池不耐用怎么办? + 助手: 您可以通过降低屏幕亮度、关闭后台应用和定期更新系统来延长电池寿命。 + + 用户问题: 这样会影响使用体验吗? + 改写后: 降低屏幕亮度和关闭后台应用是否影响使用体验 + + 示例3: + 历史对话: + 用户: 如何制作红烧肉? + 助手: 红烧肉的制作需要先将肉块焯水,然后加入酱油、糖等调料慢炖。 + + 用户问题: 需要炖多久? + 改写后: 红烧肉需要炖多久 + + 示例4: + 历史对话: + 用户: 北京到上海的高铁票价是多少? + 助手: 北京到上海的高铁票价根据车次和座位类型不同,二等座约为553元,一等座约为933元。 + + 用户问题: 时间呢? + 改写后: 北京到上海的高铁时长 + + 示例5: + 历史对话: + 用户: 如何注册微信账号? + 助手: 注册微信账号需要下载微信APP,输入手机号,接收验证码,然后设置昵称和密码。 + + 用户问题: 国外手机号可以吗? + 改写后: 国外手机号是否可以注册微信账号 + rewrite_prompt_user: | + ## 历史对话背景 + {{range .Conversation}} + ------BEGIN------ + 用户的问题是:{{.Query}} + 助手的回答是:{{.Answer}} + ------END------ + {{end}} + + ## 需要改写的用户问题 + {{.Query}} + + ## 改写后的问题 + keywords_extraction_prompt: | + # 角色 + 你是一个专业的关键词提取助手,你的任务是根据用户的问题,提取出最重要的关键词/短语。 + + # 要求 + - 总结用户的问题,并给出最重要的关键词/短语,关键词/短语的数量不超过5个 + - 使用逗号作为分隔符来分隔关键词/短语 + - 关键词/短语必须来自于用户的问题,不得虚构 + - 不要输出任何解释,直接输出关键词/短语,不要有任何前缀、解释或标点符号,不要尝试回答该问题,后面有其他助手会去搜索此问题 + + # 输出格式 + keyword1, keyword2, keyword3, keyword4, keyword5 + + # Examples + + ## Example 1 + USER: 如何提高英语口语水平? + ############### + Output: 英语口语, 口语水平, 提高英语口语, 英语口语提升, 英语口语练习 + + ## Example 2 + USER: 最近上海有什么好玩的展览活动? + ############### + Output: 上海展览, 展览活动, 上海展览推荐, 展览活动推荐, 上海展览活动 + + ## Example 3 + USER: 苹果手机电池不耐用怎么解决? + ############### + Output: 苹果手机, 电池不耐用, 电池优化, 电池寿命, 电池保养 + + ## Example 4 + USER: Python的Logo长啥样? + ############### + Output: Python Logo + + ## Example 5 + USER: 如何使用iPhone连接WiFi? + ############### + Output: iPhone, 连接WiFi, 使用iPhone连接WiFi + + # Real Data + USER: {{.Query}} + + keywords_extraction_prompt_user: | + Output: + + generate_summary_prompt: | + 你是一个精准的文章总结专家。你的任务是提取并总结用户提供的文章或片段的核心内容。 + + ## 核心要求 + - 总结结果长度为50-100个字,根据内容复杂度灵活调整 + - 完全基于提供的文章内容生成总结,不添加任何未在文章中出现的信息 + - 确保总结包含文章的关键信息点和主要结论 + - 即使文章内容较复杂或专业,也必须尝试提取核心要点进行总结 + - 直接输出总结结果,不包含任何引言、前缀或解释 + + ## 格式与风格 + - 使用客观、中立的第三人称陈述语气 + - 使用清晰简洁的中文表达 + - 保持逻辑连贯性,确保句与句之间有合理过渡 + - 避免重复使用相同的表达方式或句式结构 + + ## 注意事项 + - 绝对不输出"无法生成"、"无法总结"、"内容不足"等拒绝回应的词语 + - 不要照抄或参考示例中的任何内容,确保总结完全基于用户提供的新文章 + - 对于任何文本都尽最大努力提取重点并总结,无论长度或复杂度 + + ## 以下是用户给出的文章相关信息: + + generate_session_title_prompt: | + 你是一个专业的会话标题生成助手,你的任务是为用户提问创建简洁、精准且具描述性的标题。 + + ## 格式要求 + - 标题长度必须在10个字以内 + - 标题应准确反映用户问题的核心主题 + - 使用名词短语结构,避免使用问句 + - 保持简洁明了,删除非必要词语 + - 不要使用"关于"、"如何"等冗余词语开头 + - 直接输出标题文本,不要有任何前缀、解释或标点符号 + + ## Few-shot示例 + + 用户问题: 如何提高英语口语水平? + 标题: 英语口语提升 + + 用户问题: 最近上海有什么好玩的展览活动? + 标题: 上海展览推荐 + + 用户问题: 苹果手机电池不耐用怎么解决? + 标题: 苹果电池优化 + + ## 用户的问题是: + summary: + repeat_penalty: 1.0 + temperature: 0.3 + max_completion_tokens: 2048 + no_match_prefix: |- + + + NO_MATCH + prompt: | + 这是用户和助手之间的对话。当用户提出问题时,助手会基于特定的信息进行解答。助手首先在心中思考推理过程,然后向用户提供答案。 + 推理过程用 标签包围,答案直接输出在think标签后面,即: + + 这里是推理过程 + + 这里是答案 + context_template: | + 你是一个专业的智能信息检索助手,名为小微,犹如专业的高级秘书,依据检索到的信息回答用户问题。 + 当用户提出问题时,助手只能基于给定的信息进行解答,不能利用任何先验知识。 + + ## 回答问题规则 + - 仅根据检索到的信息中的事实进行回复,不得运用任何先验知识,保持回应的客观性和准确性。 + - 复杂问题和答案的按Markdown分结构展示,总述部分不需要拆分 + - 如果是比较简单的答案,不需要把最终答案拆分的过于细碎 + - 结果中使用的图片地址必须来自于检索到的信息,不得虚构 + - 检查结果中的文字和图片是否来自于检索到的信息,如果扩展了不在检索到的信息中的内容,必须进行修改,直到得到最终答案 + - 如果用户问题无法回答,只输出NO_MATCH即可,即: + + + NO_MATCH + + ## 输出限制 + - 以Markdown图文格式输出你的最终结果 + - 输出内容要保证简短且全面,条理清晰,信息明确,不重复。 + + ## 当前时间是: + {{.CurrentTime}} {{.CurrentWeek}} + + ## 检索到的信息如下: + ------BEGIN------ + {{range .Contexts}} + {{.}} + {{end}} + ------END------ + + ## 用户当前的问题是: + {{.Query}} + extract_entities_prompt: | + ## 任务 + 用户提供的文本中,提取所有符合以下实体类型的实体: + EntityTypes: [Person, Organization, Location, Product, Event, Date, Work, Concept, Resource, Category, Operation] + + ## 要求 + 1. 提取结果必须以JSON数组格式输出 + 2. 每个实体必须包含 title 和 type 字段,description 字段可选但强烈建议提供 + 3. 确保 type 字段的值必须严格从 EntityTypes 列表中选择,不得创建新类型 + 4. 如果无法确定实体类型,不要强行归类,宁可不提取该实体 + 5. 不要输出任何解释或额外内容,只输出JSON数组 + 6. 所有字段值不能包含HTML标签或其他代码 + 7. 如果实体有歧义,需在description中说明具体指代 + 8. 若没有找到任何实体,返回空数组 [] + + ## 实体提取规则 + - Person: 真实或虚构的人物,包括历史人物、现代人物、文学角色等 + - Organization: 公司、政府机构、团队、学校等组织实体 + - Location: 地理位置、地标、国家、城市等 + - Product: 商品、服务、品牌等商业产品 + - Event: 事件、会议、节日、历史事件等 + - Date: 日期、时间段、年代等时间相关信息 + - Work: 书籍、电影、音乐、艺术作品等创作内容 + - Concept: 抽象概念、思想、理论等 + - Resource: 自然资源、信息资源、工具等 + - Category: 分类、类别、领域等 + - Operation: 操作、动作、方法、过程等 + + ## 提取步骤 + 1. 仔细阅读文本,识别可能的实体 + 2. 对每个识别到的实体,确定其最适合的实体类型(必须从EntityTypes中选择) + 3. 为每个实体创建包含以下字段的JSON对象: + - title: 实体的标准名称,不包含修饰词,如引号等 + - type: 从EntityTypes中选择的实体类型 + - description: 对该实体的简明中文描述,应基于文本内容 + 4. 验证每个实体的所有字段是否正确且格式化恰当 + 5. 将所有实体对象合并为一个JSON数组 + 6. 检查最终JSON是否有效并符合要求 + + ## 示例 + [输入] + 文本: 《红楼梦》,又名《石头记》,是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著,后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景,以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间(1743年前后),是中国文学史上现实主义的高峰,对后世影响深远。 + + [输出] + [ + { + "title": "红楼梦", + "type": "Work", + "description": "红楼梦是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书" + }, + { + "title": "石头记", + "type": "Work", + "description": "石头记是红楼梦的别名" + }, + { + "title": "曹雪芹", + "type": "Person", + "description": "曹雪芹是清代作家,红楼梦的作者,创作了前80回" + }, + { + "title": "高鹗", + "type": "Person", + "description": "高鹗是红楼梦后40回的续作者" + }, + { + "title": "贾宝玉", + "type": "Person", + "description": "贾宝玉是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "林黛玉", + "type": "Person", + "description": "林黛玉是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "薛宝钗", + "type": "Person", + "description": "薛宝钗是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "金陵十二钗", + "type": "Concept", + "description": "金陵十二钗是红楼梦中以贾宝玉为中心的十二位主要女性角色" + }, + { + "title": "乾隆年间", + "type": "Date", + "description": "乾隆年间指的是红楼梦成书的时间,约1743年前后" + }, + { + "title": "四大家族", + "type": "Concept", + "description": "四大家族是红楼梦中的贾、史、王、薛四个家族,是小说的背景" + }, + { + "title": "中国文学史", + "type": "Category", + "description": "红楼梦被视为中国文学史中现实主义的高峰之作" + } + ] + + extract_relationships_prompt: | + ## 任务 + 从用户提供的实体数组中,提取实体之间存在的明确关系,形成结构化的关系网络。 + + ## 要求 + 1. 关系提取必须基于提供的文本内容,不得臆测不存在的关系 + 2. 结果必须以JSON数组格式输出,每个关系为数组中的一个对象 + 3. 每个关系对象必须包含 source, target, description 和 strength 字段 + 4. 不要输出任何解释或额外内容,只输出JSON数组 + 5. 若没有找到任何关系,返回空数组 [] + + ## 关系提取规则 + - 只有在文本中明确体现的关系才应被提取 + - 源实体(source)和目标实体(target)必须是实体数组中已有的实体 + - 关系描述(description)应简明扼要地说明两个实体间的具体关系 + - 关系强度(strength)应根据以下标准确定: + * 10分:直接创造/从属关系(如作者与作品、发明者与发明、母公司与子公司) + * 9分:同一实体的不同表现形式(如别名、曾用名) + * 8分:紧密相关且互相影响的关系(如密切合作伙伴、家庭成员) + * 7分:明确但非直接的关系(如作品中的角色、组织中的成员) + * 6分:间接关联且有明确联系(如同事关系、相似产品) + * 5分:存在关联但较为松散(如同一领域的不同概念) + + ## 提取步骤 + 1. 仔细分析文本内容,确定哪些实体之间存在明确关系 + 2. 只考虑文本中明确提及的关系,不要臆测 + 3. 对每个找到的关系,确定: + - source: 关系的源实体标题(必须是实体列表中已有的实体) + - target: 关系的目标实体标题(必须是实体列表中已有的实体) + - description: 简明准确的关系描述(用中文表述) + - strength: 基于上述标准的关系强度(5-10之间的整数) + 4. 检查每个关系是否双向: + - 如果关系是双向的(如"A是B的朋友"意味着"B也是A的朋友"),考虑是否需要创建反向关系 + - 如果关系是单向的(如"A创作了B"),则只保留单向关系 + 5. 验证所有关系的一致性和合理性: + - 确保没有矛盾的关系(如A同时是B的父亲和兄弟) + - 确保关系描述与关系强度匹配 + 6. 将所有有效关系组织为JSON数组 + + ## 示例 + [输入] + 实体: [ + { + "title": "红楼梦", + "type": "Work", + "description": "红楼梦是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书" + }, + { + "title": "石头记", + "type": "Work", + "description": "石头记是红楼梦的别名" + }, + { + "title": "曹雪芹", + "type": "Person", + "description": "曹雪芹是清代作家,红楼梦的作者,创作了前80回" + }, + { + "title": "高鹗", + "type": "Person", + "description": "高鹗是红楼梦后40回的续作者" + }, + { + "title": "贾宝玉", + "type": "Person", + "description": "贾宝玉是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "林黛玉", + "type": "Person", + "description": "林黛玉是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "薛宝钗", + "type": "Person", + "description": "薛宝钗是红楼梦中的主要角色,爱情悲剧的主角之一" + }, + { + "title": "四大家族", + "type": "Concept", + "description": "四大家族是红楼梦中的贾、史、王、薛四个家族,是小说的背景" + }, + { + "title": "金陵十二钗", + "type": "Concept", + "description": "金陵十二钗是红楼梦中以贾宝玉为中心的十二位主要女性角色" + }, + { + "title": "乾隆年间", + "type": "Date", + "description": "乾隆年间指的是红楼梦成书的时间,约1743年前后" + }, + { + "title": "中国文学史", + "type": "Category", + "description": "红楼梦被视为中国文学史中现实主义的高峰之作" + } + ] + + 文本: 《红楼梦》,又名《石头记》,是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著,后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景,以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间(1743年前后),是中国文学史上现实主义的高峰,对后世影响深远。 + + [输出] + [ + { + "source": "曹雪芹", + "target": "红楼梦", + "description": "曹雪芹是红楼梦的主要作者,创作了前80回", + "strength": 10 + }, + { + "source": "高鹗", + "target": "红楼梦", + "description": "高鹗是红楼梦后40回的续作者", + "strength": 10 + }, + { + "source": "红楼梦", + "target": "石头记", + "description": "石头记是红楼梦的别名", + "strength": 9 + }, + { + "source": "红楼梦", + "target": "中国文学史", + "description": "红楼梦被视为中国文学史中现实主义的高峰之作", + "strength": 7 + }, + { + "source": "贾宝玉", + "target": "林黛玉", + "description": "贾宝玉与林黛玉有深厚的爱情关系,是小说主线之一", + "strength": 8 + }, + { + "source": "贾宝玉", + "target": "薛宝钗", + "description": "贾宝玉与薛宝钗的关系是小说爱情悲剧主线的一部分", + "strength": 8 + }, + { + "source": "贾宝玉", + "target": "金陵十二钗", + "description": "贾宝玉是金陵十二钗故事的中心人物", + "strength": 8 + }, + { + "source": "红楼梦", + "target": "贾宝玉", + "description": "贾宝玉是红楼梦中的主要角色", + "strength": 7 + }, + { + "source": "红楼梦", + "target": "林黛玉", + "description": "林黛玉是红楼梦中的主要角色", + "strength": 7 + }, + { + "source": "红楼梦", + "target": "薛宝钗", + "description": "薛宝钗是红楼梦中的主要角色", + "strength": 7 + }, + { + "source": "红楼梦", + "target": "四大家族", + "description": "四大家族是红楼梦的背景设定", + "strength": 7 + }, + { + "source": "红楼梦", + "target": "金陵十二钗", + "description": "金陵十二钗是红楼梦中的重要概念", + "strength": 7 + }, + { + "source": "红楼梦", + "target": "乾隆年间", + "description": "红楼梦成书于乾隆年间,约1743年前后", + "strength": 6 + } + ] + +# 知识库配置 +knowledge_base: + chunk_size: 512 + chunk_overlap: 50 + split_markers: ["\n\n", "\n", "。"] + image_processing: + enable_multimodal: true diff --git a/config/config.yaml b/config/config.yaml index d739a83..9a71c1c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -205,30 +205,19 @@ conversation: NO_MATCH prompt: | - 这是用户和助手之间的对话。当用户提出问题时,助手会基于特定的信息进行解答。助手首先在心中思考推理过程,然后向用户提供答案。 - 推理过程用 标签包围,答案直接输出在think标签后面,即: - - 这里是推理过程 - - 这里是答案 + 这是用户和助手之间的对话。当用户提出问题时,助手会基于特定的信息进行解答。直接提供答案,没有思考过程 context_template: | - 你是一个专业的智能信息检索助手,名为小微,犹如专业的高级秘书,依据检索到的信息回答用户问题。 - 当用户提出问题时,助手只能基于给定的信息进行解答,不能利用任何先验知识。 - ## 回答问题规则 - - 仅根据检索到的信息中的事实进行回复,不得运用任何先验知识,保持回应的客观性和准确性。 - - 复杂问题和答案的按Markdown分结构展示,总述部分不需要拆分 - - 如果是比较简单的答案,不需要把最终答案拆分的过于细碎 - - 结果中使用的图片地址必须来自于检索到的信息,不得虚构 - - 检查结果中的文字和图片是否来自于检索到的信息,如果扩展了不在检索到的信息中的内容,必须进行修改,直到得到最终答案 - - 如果用户问题无法回答,只输出NO_MATCH即可,即: - - - NO_MATCH + - 回答问题仅依据检索到的信息中的事实,不利用任何先验知识。 + - 复杂问题与答案采用Markdown分结构展示,总述部分不拆分。 + - 简单答案无需过度拆分 + - 结果中使用的图片地址必须来自检索到的信息,不得虚构。 + - 检查文字和图片是否均来自检索到的信息,若扩展了不在其中的内容,需修改至符合要求。 + - 若用户问题无法回答,只输出“NO_MATCH”。 ## 输出限制 - - 以Markdown图文格式输出你的最终结果 - - 输出内容要保证简短且全面,条理清晰,信息明确,不重复。 + - 以Markdown图文格式输出最终结果。 + - 输出内容简短全面、条理清晰、信息明确、无重复。 ## 当前时间是: {{.CurrentTime}} {{.CurrentWeek}} @@ -534,3 +523,16 @@ knowledge_base: split_markers: ["\n\n", "\n", "。"] image_processing: enable_multimodal: true + +#{"chunk_size": 1000, "separators": ["============================= "], "chunk_overlap": 200, "enable_multimodal": true} +# 知识库配置 +init_simple_conf: + knowledge_base: + chunk_size: 512 + chunk_overlap: 50 + split_markers: [ "\n\n", "\n", "。" ] + image_processing: + enable_multimodal: true + emb_model: "6e500e62-63c0-4b87-ae10-4d6f2fa83dd5" + chat_model: "3f733012-b496-4025-9ff2-30bf5e0f595c" + diff --git a/internal/application/service/chat_pipline/common.go b/internal/application/service/chat_pipline/common.go index 2c75e67..d153cdb 100644 --- a/internal/application/service/chat_pipline/common.go +++ b/internal/application/service/chat_pipline/common.go @@ -30,6 +30,7 @@ func prepareChatModel(ctx context.Context, modelService interfaces.ModelService, MaxCompletionTokens: chatManage.SummaryConfig.MaxCompletionTokens, FrequencyPenalty: chatManage.SummaryConfig.FrequencyPenalty, PresencePenalty: chatManage.SummaryConfig.PresencePenalty, + Thinking: chatManage.SummaryConfig.Thinking, } return chatModel, opt, nil diff --git a/internal/application/service/common_test.go b/internal/application/service/common_test.go index 2ae1f39..ac25b58 100644 --- a/internal/application/service/common_test.go +++ b/internal/application/service/common_test.go @@ -1,27 +1,88 @@ package service import ( - "context" - - "knowlege-lsxd/internal/types" - + "fmt" + "knowlege-lsxd/internal/application/repository" + "knowlege-lsxd/internal/config" + "knowlege-lsxd/internal/models/utils/ollama" "knowlege-lsxd/internal/types/interfaces" + "os" + "time" - "github.com/stretchr/testify/mock" + "gorm.io/driver/postgres" + "gorm.io/gorm" ) -type MockKnowledgeBaseRepo struct { - mock.Mock - interfaces.KnowledgeBaseRepository +var Repo = setRepo() + +type repos struct { + Chunk interfaces.ChunkRepository + KnowlegeBase interfaces.KnowledgeBaseRepository + Knowlege interfaces.KnowledgeRepository + Model interfaces.ModelRepository + Tenants interfaces.TenantRepository } -func (m *MockKnowledgeBaseRepo) Create(ctx context.Context, kb *types.KnowledgeBase) error { - args := m.Called(ctx, kb) - return args.Error(0) -} - -func must(err error) { +func setRepo() *repos { + config.SetEnv() + database, err := InitDatabase(nil) if err != nil { panic(err) } + + return &repos{ + Chunk: repository.NewChunkRepository(database), + KnowlegeBase: repository.NewKnowledgeBaseRepository(database), + Knowlege: repository.NewKnowledgeRepository(database), + Model: repository.NewModelRepository(database), + Tenants: repository.NewTenantRepository(database), + } +} + +// Ollama 函数用于获取 OllamaService 实例 +// 如果获取过程中出现错误,会通过 panic 抛出异常 +// 返回值: *ollama.OllamaService - Ollama 服务的实例指针 +func Ollama() *ollama.OllamaService { + // 调用 ollama 包的 GetOllamaService 函数获取服务实例 + ol, e := ollama.GetOllamaService() + if e != nil { + panic(e) + } + return ol +} + +func InitDatabase(cfg *config.Config) (*gorm.DB, error) { + var dialector gorm.Dialector + driver := os.Getenv("DB_DRIVER") + switch driver { + case "postgres": + dsn := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"), + os.Getenv("DB_USER"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_NAME"), + "disable", + ) + dialector = postgres.Open(dsn) + default: + return nil, fmt.Errorf("unsupported database driver: %s", os.Getenv("DB_DRIVER")) + } + db, err := gorm.Open(dialector, &gorm.Config{}) + if err != nil { + return nil, err + } + + // Get underlying SQL DB object + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // Configure connection pool parameters + sqlDB.SetMaxIdleConns(10) + sqlDB.SetConnMaxLifetime(time.Duration(10) * time.Minute) + + return db, nil } diff --git a/internal/application/service/knowledgebase.go b/internal/application/service/knowledgebase.go index d5ac862..63fc95c 100644 --- a/internal/application/service/knowledgebase.go +++ b/internal/application/service/knowledgebase.go @@ -3,18 +3,20 @@ package service import ( "context" "errors" + "knowlege-lsxd/internal/config" "slices" "time" "encoding/json" - "github.com/google/uuid" "knowlege-lsxd/internal/application/service/retriever" "knowlege-lsxd/internal/common" "knowlege-lsxd/internal/logger" "knowlege-lsxd/internal/models/embedding" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" + + "github.com/google/uuid" ) // ErrInvalidTenantID represents an error for invalid tenant ID @@ -29,7 +31,9 @@ type knowledgeBaseService struct { } // NewKnowledgeBaseService creates a new knowledge base service -func NewKnowledgeBaseService(repo interfaces.KnowledgeBaseRepository, +func NewKnowledgeBaseService( + cfg *config.Config, + repo interfaces.KnowledgeBaseRepository, kgRepo interfaces.KnowledgeRepository, chunkRepo interfaces.ChunkRepository, modelService interfaces.ModelService, @@ -258,13 +262,23 @@ func (s *knowledgeBaseService) CopyKnowledgeBase(ctx context.Context, return sourceKB, targetKB, nil } -// HybridSearch performs hybrid search, including vector retrieval and keyword retrieval +// HybridSearch 执行混合搜索,结合向量和关键词检索方法 +// 参数: +// - ctx: 上下文,用于传递请求信息和控制执行 +// - id: 知识库ID +// - params: 搜索参数,包含查询文本、匹配数量等 +// +// 返回值: +// - []*types.SearchResult: 搜索结果列表 +// - error: 错误信息 func (s *knowledgeBaseService) HybridSearch(ctx context.Context, id string, params types.SearchParams, ) ([]*types.SearchResult, error) { + // 记录混合搜索的参数信息,包括知识库ID和查询文本 logger.Infof(ctx, "Hybrid search parameters, knowledge base ID: %s, query text: %s", id, params.QueryText) + // 从上下文中获取租户信息 tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant) logger.Infof(ctx, "Creating composite retrieval engine, tenant ID: %d", tenantInfo.ID) @@ -374,43 +388,56 @@ func (s *knowledgeBaseService) HybridSearch(ctx context.Context, return s.processSearchResults(ctx, deduplicatedChunks) } -// processSearchResults handles the processing of search results, optimizing database queries +// processSearchResults 处理搜索结果的方法 +// 参数: +// +// ctx - 上下文,用于传递请求相关的信息和控制请求的生命周期 +// chunks - 带有分数的索引块列表 +// +// 返回值: +// +// []*types.SearchResult - 处理后的搜索结果列表 +// error - 处理过程中可能出现的错误 func (s *knowledgeBaseService) processSearchResults(ctx context.Context, chunks []*types.IndexWithScore) ([]*types.SearchResult, error) { + // 如果输入的块列表为空,直接返回nil if len(chunks) == 0 { return nil, nil } + // 从上下文中获取租户ID tenantID := ctx.Value(types.TenantIDContextKey).(uint) - // Prepare data structures for efficient processing - var knowledgeIDs []string - var chunkIDs []string - chunkScores := make(map[string]float64) - chunkMatchTypes := make(map[string]types.MatchType) - processedKnowledgeIDs := make(map[string]bool) + // 初始化必要的变量 + var knowledgeIDs []string // 知识ID列表 + var chunkIDs []string // 块ID列表 + chunkScores := make(map[string]float64) // 块ID到分数的映射 + chunkMatchTypes := make(map[string]types.MatchType) // 块ID到匹配类型的映射 + processedKnowledgeIDs := make(map[string]bool) // 已处理的知识ID集合 - // Collect all knowledge and chunk IDs + // 遍历处理输入的块列表 for _, chunk := range chunks { + // 如果知识ID未处理过,则添加到知识ID列表 if !processedKnowledgeIDs[chunk.KnowledgeID] { knowledgeIDs = append(knowledgeIDs, chunk.KnowledgeID) processedKnowledgeIDs[chunk.KnowledgeID] = true } + // 添加块ID到块ID列表,并记录分数和匹配类型 chunkIDs = append(chunkIDs, chunk.ChunkID) chunkScores[chunk.ChunkID] = chunk.Score chunkMatchTypes[chunk.ChunkID] = chunk.MatchType } - // Batch fetch knowledge data + // 获取知识数据 logger.Infof(ctx, "Fetching knowledge data for %d IDs", len(knowledgeIDs)) knowledgeMap, err := s.fetchKnowledgeData(ctx, tenantID, knowledgeIDs) if err != nil { return nil, err } - // Batch fetch all chunks in one go + // 获取块数据 logger.Infof(ctx, "Fetching chunk data for %d IDs", len(chunkIDs)) allChunks, err := s.chunkRepo.ListChunksByID(ctx, tenantID, chunkIDs) if err != nil { @@ -421,34 +448,33 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, return nil, err } - // Build chunk map and collect additional IDs to fetch + // 创建块映射并处理相关块 chunkMap := make(map[string]*types.Chunk, len(allChunks)) var additionalChunkIDs []string processedChunkIDs := make(map[string]bool) - // First pass: Build chunk map and collect parent IDs for _, chunk := range allChunks { + // 将块添加到块映射中 chunkMap[chunk.ID] = chunk processedChunkIDs[chunk.ID] = true - // Collect parent chunks + // 处理父块 if chunk.ParentChunkID != "" && !processedChunkIDs[chunk.ParentChunkID] { additionalChunkIDs = append(additionalChunkIDs, chunk.ParentChunkID) processedChunkIDs[chunk.ParentChunkID] = true - // Pass score to parent chunkScores[chunk.ParentChunkID] = chunkScores[chunk.ID] chunkMatchTypes[chunk.ParentChunkID] = types.MatchTypeParentChunk } - // Collect related chunks + // 处理相关块 relationChunkIDs := s.collectRelatedChunkIDs(chunk, processedChunkIDs) for _, chunkID := range relationChunkIDs { additionalChunkIDs = append(additionalChunkIDs, chunkID) chunkMatchTypes[chunkID] = types.MatchTypeRelationChunk } - // Add nearby chunks (prev and next) + // 处理文本类型块的相邻块 if chunk.ChunkType == types.ChunkTypeText { if chunk.NextChunkID != "" && !processedChunkIDs[chunk.NextChunkID] { additionalChunkIDs = append(additionalChunkIDs, chunk.NextChunkID) @@ -463,33 +489,37 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, } } - // Fetch all additional chunks in one go if needed + // 获取额外的块数据 if len(additionalChunkIDs) > 0 { logger.Infof(ctx, "Fetching %d additional chunks", len(additionalChunkIDs)) additionalChunks, err := s.chunkRepo.ListChunksByID(ctx, tenantID, additionalChunkIDs) if err != nil { logger.Warnf(ctx, "Failed to fetch some additional chunks: %v", err) - // Continue with what we have + } else { - // Add to chunk map + + // 将获取到的额外块添加到块映射中 for _, chunk := range additionalChunks { chunkMap[chunk.ID] = chunk } } } - // Build final search results + // 构建搜索结果 var searchResults []*types.SearchResult for chunkID, chunk := range chunkMap { + // 检查是否为有效的文本块 if !s.isValidTextChunk(chunk) { continue } + // 获取块的分数 score, hasScore := chunkScores[chunkID] if !hasScore || score <= 0 { score = 0.0 } + // 构建搜索结果 if knowledge, ok := knowledgeMap[chunk.KnowledgeID]; ok { matchType := types.MatchTypeParentChunk if specificType, exists := chunkMatchTypes[chunkID]; exists { diff --git a/internal/application/service/knowledgebase_test.go b/internal/application/service/knowledgebase_test.go index ba6658f..28e15c2 100644 --- a/internal/application/service/knowledgebase_test.go +++ b/internal/application/service/knowledgebase_test.go @@ -1,37 +1,18 @@ package service import ( + "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "knowlege-lsxd/internal/container" - "knowlege-lsxd/internal/runtime" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" ) -func Test_CreateKnowledge(t *testing.T) { - // 1. 构建容器 - container := container.BuildContainer(runtime.GetContainer()) - - // 2. Mock 依赖 - mockKBRepo := new(MockKnowledgeBaseRepo) - must(container.Provide(func() interfaces.KnowledgeBaseRepository { - return mockKBRepo - })) - - // 3. 解析服务(自动注入 Mock 的 KnowledgeBaseRepository) - var service *knowledgeBaseService - must(container.Invoke(func(k *knowledgeBaseService) { - service = k - })) - - // 4. 设置 Mock 预期 - mockKBRepo.On("Create", context.Background(), mock.Anything).Return(nil) - - // 5. 调用并断言 - base, err := service.CreateKnowledgeBase(context.Background(), &types.KnowledgeBase{Name: "Test"}) - assert.NoError(t, err, base) - mockKBRepo.AssertExpectations(t) +func KnowledgeBase() interfaces.KnowledgeBaseService { + return NewKnowledgeBaseService(Repo.KnowlegeBase, Repo.Knowlege, Repo.Chunk, Model()) +} + +func Test_CreateKnowledge(t *testing.T) { + res, err := KnowledgeBase().CreateKnowledgeBase(context.Background(), &types.KnowledgeBase{}) + t.Log(res, err) } diff --git a/internal/application/service/model_test.go b/internal/application/service/model_test.go new file mode 100644 index 0000000..86725cf --- /dev/null +++ b/internal/application/service/model_test.go @@ -0,0 +1,15 @@ +package service + +import ( + "knowlege-lsxd/internal/types/interfaces" +) + +func Model() interfaces.ModelService { + + return NewModelService(Repo.Model, Ollama()) +} + +func a() { + s := `## 回答问题规则\n - 回答问题仅依据检索到的信息中的事实,不利用任何先验知识。\n - 复杂问题与答案采用Markdown分结构展示,总述部分不拆分。\n - 简单答案无需过度拆分\n - 结果中使用的图片地址必须来自检索到的信息,不得虚构。\n - 检查文字和图片是否均来自检索到的信息,若扩展了不在其中的内容,需修改至符合要求。\n - 若用户问题无法回答,只输出“NO_MATCH”。\n\n ## 输出限制\n - 以Markdown图文格式输出最终结果。\n - 输出内容简短全面、条理清晰、信息明确、无重复。\n\n ## 当前时间是:\n {{.CurrentTime}} {{.CurrentWeek}}\n\n ## 检索到的信息如下:\n ------BEGIN------\n {{range .Contexts}}\n {{.}}\n {{end}}\n ------END------\n\n ## 用户当前的问题是:\n {{.Query}}` + +} diff --git a/internal/application/service/session.go b/internal/application/service/session.go index 426e8c2..0ed14d9 100644 --- a/internal/application/service/session.go +++ b/internal/application/service/session.go @@ -5,8 +5,6 @@ import ( "errors" "strings" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" chatpipline "knowlege-lsxd/internal/application/service/chat_pipline" "knowlege-lsxd/internal/config" "knowlege-lsxd/internal/logger" @@ -14,6 +12,9 @@ import ( "knowlege-lsxd/internal/tracing" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" ) // sessionService implements the SessionService interface for managing conversation sessions @@ -309,8 +310,6 @@ func (s *sessionService) GenerateTitle(ctx context.Context, func (s *sessionService) KnowledgeQA(ctx context.Context, sessionID, query string) ( []*types.SearchResult, <-chan types.StreamResponse, error, ) { - logger.Info(ctx, "Start knowledge base question answering") - logger.Infof(ctx, "Knowledge base question answering parameters, session ID: %s, query: %s", sessionID, query) // Get tenant ID from context tenantID := ctx.Value(types.TenantIDContextKey).(uint) @@ -375,6 +374,70 @@ func (s *sessionService) KnowledgeQA(ctx context.Context, sessionID, query strin return chatManage.MergeResult, chatManage.ResponseChan, nil } +// KnowledgeQAQuick performs knowledge base question answering with LLM summarization +func (s *sessionService) KnowledgeQAQuick(ctx context.Context, sessionID, query string) ( + []*types.SearchResult, string, error, +) { + + // Get tenant ID from context + tenantID := ctx.Value(types.TenantIDContextKey).(uint) + logger.Infof(ctx, "Getting session info, session ID: %s, tenant ID: %d", sessionID, tenantID) + + // Get session information + session, err := s.sessionRepo.Get(ctx, tenantID, sessionID) + if err != nil { + logger.Errorf(ctx, "Failed to get session, session ID: %s, error: %v", sessionID, err) + return nil, "", err + } + + // Validate knowledge base association + if session.KnowledgeBaseID == "" { + logger.Warnf(ctx, "Session has no associated knowledge base, session ID: %s", sessionID) + return nil, "", errors.New("session has no knowledge base") + } + chatManage := &types.ChatManage{ + Query: query, + RewriteQuery: query, + SessionID: sessionID, + KnowledgeBaseID: session.KnowledgeBaseID, + VectorThreshold: session.VectorThreshold, + KeywordThreshold: session.KeywordThreshold, + EmbeddingTopK: session.EmbeddingTopK, + RerankModelID: session.RerankModelID, + RerankTopK: session.RerankTopK, + RerankThreshold: session.RerankThreshold, + ChatModelID: session.SummaryModelID, + SummaryConfig: types.SummaryConfig{ + MaxTokens: session.SummaryParameters.MaxTokens, + RepeatPenalty: session.SummaryParameters.RepeatPenalty, + TopK: session.SummaryParameters.TopK, + TopP: session.SummaryParameters.TopP, + FrequencyPenalty: session.SummaryParameters.FrequencyPenalty, + PresencePenalty: session.SummaryParameters.PresencePenalty, + Prompt: session.SummaryParameters.Prompt, + ContextTemplate: session.SummaryParameters.ContextTemplate, + Temperature: session.SummaryParameters.Temperature, + Seed: session.SummaryParameters.Seed, + NoMatchPrefix: session.SummaryParameters.NoMatchPrefix, + MaxCompletionTokens: session.SummaryParameters.MaxCompletionTokens, + Thinking: session.SummaryParameters.Thinking, + }, + FallbackResponse: session.FallbackResponse, + } + + err = s.KnowledgeQAByEvent(ctx, chatManage, types.Pipline["rag"]) + if err != nil { + logger.ErrorWithFields(ctx, err, map[string]interface{}{ + "session_id": sessionID, + "knowledge_base_id": session.KnowledgeBaseID, + }) + return nil, "", err + } + + logger.Info(ctx, "Knowledge base question answering completed") + return chatManage.MergeResult, chatManage.ChatResponse.Content, nil +} + // KnowledgeQAByEvent processes knowledge QA through a series of events in the pipeline func (s *sessionService) KnowledgeQAByEvent(ctx context.Context, chatManage *types.ChatManage, eventList []types.EventType, diff --git a/internal/config/config.go b/internal/config/config.go index d74781b..0a7d9ee 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,13 @@ type Config struct { VectorDatabase *VectorDatabaseConfig `yaml:"vector_database" json:"vector_database"` DocReader *DocReaderConfig `yaml:"docreader" json:"docreader"` StreamManager *StreamManagerConfig `yaml:"stream_manager" json:"stream_manager"` + InitSimpleConf *InitSimpleConf `yaml:"init_simple_conf" json:"init_simple_conf"` +} + +type InitSimpleConf struct { + EmbModel string `yaml:"emb_model" json:"emb_model"` + ChatModel string `yaml:"chat_model" json:"chat_model"` + KnowledgeBase *KnowledgeBaseConfig `yaml:"knowledge_base" json:"knowledge_base"` } type DocReaderConfig struct { diff --git a/internal/container/container.go b/internal/container/container.go index e4ab614..27d55ff 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -55,7 +55,7 @@ func BuildContainer(container *dig.Container) *dig.Container { // Core infrastructure configuration must(container.Provide(config.LoadConfig)) must(container.Provide(initTracer)) - must(container.Provide(initDatabase)) + must(container.Provide(InitDatabase)) must(container.Provide(initFileService)) must(container.Provide(initAntsPool)) @@ -146,7 +146,7 @@ func initTracer() (*tracing.Tracer, error) { return tracing.InitTracer() } -// initDatabase initializes database connection +// InitDatabase initializes database connection // Creates and configures database connection based on environment configuration // Supports multiple database backends (PostgreSQL) // Parameters: @@ -155,7 +155,7 @@ func initTracer() (*tracing.Tracer, error) { // Returns: // - Configured database connection // - Error if connection fails -func initDatabase(cfg *config.Config) (*gorm.DB, error) { +func InitDatabase(cfg *config.Config) (*gorm.DB, error) { var dialector gorm.Dialector driver := os.Getenv("DB_DRIVER") switch driver { diff --git a/internal/handler/initialization.go b/internal/handler/initialization.go index 03a3798..3a7e9c7 100644 --- a/internal/handler/initialization.go +++ b/internal/handler/initialization.go @@ -13,9 +13,6 @@ import ( "strconv" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/ollama/ollama/api" "knowlege-lsxd/internal/config" "knowlege-lsxd/internal/errors" "knowlege-lsxd/internal/logger" @@ -25,6 +22,10 @@ import ( "knowlege-lsxd/internal/types/interfaces" "knowlege-lsxd/services/docreader/src/client" "knowlege-lsxd/services/docreader/src/proto" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/ollama/ollama/api" ) // DownloadTask 下载任务信息 @@ -250,6 +251,7 @@ func (h *InitializationHandler) Initialize(c *gin.Context) { c.Error(errors.NewBadRequestError("分块重叠大小必须小于分块大小")) return } + if len(req.DocumentSplitting.Separators) == 0 { logger.Error(ctx, "Document separators cannot be empty") c.Error(errors.NewBadRequestError("文档分隔符不能为空")) diff --git a/internal/handler/knowledge.go b/internal/handler/knowledge.go index 89c5e35..fb0298d 100644 --- a/internal/handler/knowledge.go +++ b/internal/handler/knowledge.go @@ -7,11 +7,12 @@ import ( "net/http" "strconv" - "github.com/gin-gonic/gin" "knowlege-lsxd/internal/errors" "knowlege-lsxd/internal/logger" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" + + "github.com/gin-gonic/gin" ) // KnowledgeHandler processes HTTP requests related to knowledge resources @@ -82,8 +83,12 @@ func (h *KnowledgeHandler) handleDuplicateKnowledgeError(c *gin.Context, } // CreateKnowledgeFromFile handles requests to create knowledge from an uploaded file +/** + * 从文件创建知识的处理函数 + * @param c Gin上下文对象,包含请求和响应信息 + */ func (h *KnowledgeHandler) CreateKnowledgeFromFile(c *gin.Context) { - ctx := c.Request.Context() + ctx := c.Request.Context() // 获取请求上下文 // 记录开始创建知识的日志 logger.Info(ctx, "Start creating knowledge from file") // Validate access to the knowledge base diff --git a/internal/handler/knowledgebase.go b/internal/handler/knowledgebase.go index f293d6c..7e43338 100644 --- a/internal/handler/knowledgebase.go +++ b/internal/handler/knowledgebase.go @@ -1,27 +1,33 @@ package handler import ( + "knowlege-lsxd/internal/config" "net/http" - "github.com/gin-gonic/gin" "knowlege-lsxd/internal/errors" "knowlege-lsxd/internal/logger" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" + + "github.com/gin-gonic/gin" ) // KnowledgeBaseHandler defines the HTTP handler for knowledge base operations type KnowledgeBaseHandler struct { service interfaces.KnowledgeBaseService knowledgeService interfaces.KnowledgeService + kbService interfaces.KnowledgeBaseService + cfg *config.Config } // NewKnowledgeBaseHandler creates a new knowledge base handler instance func NewKnowledgeBaseHandler( + cfg *config.Config, service interfaces.KnowledgeBaseService, knowledgeService interfaces.KnowledgeService, + kbService interfaces.KnowledgeBaseService, ) *KnowledgeBaseHandler { - return &KnowledgeBaseHandler{service: service, knowledgeService: knowledgeService} + return &KnowledgeBaseHandler{service: service, knowledgeService: knowledgeService, kbService: kbService, cfg: cfg} } // HybridSearch handles requests to perform hybrid vector and keyword search on a knowledge base @@ -63,6 +69,54 @@ func (h *KnowledgeBaseHandler) HybridSearch(c *gin.Context) { }) } +// InitSimple 快速初始化知识库 +func (h *KnowledgeBaseHandler) InitSimple(c *gin.Context) { + ctx := c.Request.Context() + + logger.Info(ctx, "Starting InitSimple") + + var req types.InitSimpleRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.Error(ctx, "Failed to parse request parameters", err) + c.Error(errors.NewBadRequestError("Invalid request parameters").WithDetails(err.Error())) + return + } + //检查租户 + tenantID, exists := c.Get(types.TenantIDContextKey.String()) + if !exists { + logger.Error(ctx, "Failed to get tenant ID") + c.Error(errors.NewBadRequestError("Invalid request parameters")) + return + } + + kb := &types.KnowledgeBase{ + Name: req.Name, + Description: req.Description, + TenantID: tenantID.(uint), + ChunkingConfig: types.ChunkingConfig{ + ChunkSize: h.cfg.InitSimpleConf.KnowledgeBase.ChunkSize, + ChunkOverlap: h.cfg.InitSimpleConf.KnowledgeBase.ChunkOverlap, + Separators: h.cfg.InitSimpleConf.KnowledgeBase.SplitMarkers, + EnableMultimodal: h.cfg.InitSimpleConf.KnowledgeBase.ImageProcessing.EnableMultimodal, + }, + EmbeddingModelID: h.cfg.InitSimpleConf.EmbModel, + SummaryModelID: h.cfg.InitSimpleConf.ChatModel, + RerankModelID: "", + VLMModelID: "", + } + // 处理知识库 - 检查是否存在,不存在则创建,存在则更新 + kbc, err := h.kbService.CreateKnowledgeBase(ctx, kb) + if err != nil { + logger.ErrorWithFields(ctx, err, nil) + c.Error(errors.NewInternalServerError(err.Error())) + return + } + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "data": kbc, + }) +} + // CreateKnowledgeBase handles requests to create a new knowledge base func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c *gin.Context) { ctx := c.Request.Context() diff --git a/internal/handler/session.go b/internal/handler/session.go index f62d234..2bbd1d8 100644 --- a/internal/handler/session.go +++ b/internal/handler/session.go @@ -7,13 +7,14 @@ import ( "net/http" "time" - "github.com/gin-gonic/gin" "knowlege-lsxd/internal/application/service" "knowlege-lsxd/internal/config" "knowlege-lsxd/internal/errors" "knowlege-lsxd/internal/logger" "knowlege-lsxd/internal/types" "knowlege-lsxd/internal/types/interfaces" + + "github.com/gin-gonic/gin" ) // SessionHandler handles all HTTP requests related to conversation sessions @@ -784,6 +785,7 @@ func (h *SessionHandler) KnowledgeQA(c *gin.Context) { logger.GetLogger(ctx).Error("Complete stream failed", "error", err) } }() + for response := range respCh { response.ID = requestID c.SSEvent("message", response) @@ -801,6 +803,65 @@ func (h *SessionHandler) KnowledgeQA(c *gin.Context) { }() } +func (h *SessionHandler) KnowledgeQuick(c *gin.Context) { + ctx := logger.CloneContext(c.Request.Context()) + sessionID := c.Param("session_id") + if sessionID == "" { + c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error())) + return + } + + var request CreateKnowledgeQARequest + if err := c.ShouldBindJSON(&request); err != nil { + c.Error(errors.NewBadRequestError(err.Error())) + return + } + + assistantMessage := &types.Message{ + SessionID: sessionID, + Role: "assistant", + RequestID: c.GetString(types.RequestIDContextKey.String()), + IsCompleted: false, + } + defer h.completeAssistantMessage(ctx, assistantMessage) + + if request.Query == "" { + c.Error(errors.NewBadRequestError("Query content cannot be empty")) + return + } + + if _, err := h.messageService.CreateMessage(ctx, &types.Message{ + SessionID: sessionID, + Role: "user", + Content: request.Query, + RequestID: c.GetString(types.RequestIDContextKey.String()), + CreatedAt: time.Now(), + IsCompleted: true, + }); err != nil { + c.Error(errors.NewInternalServerError(err.Error())) + return + } + assistantMessage.CreatedAt = time.Now() + if _, err := h.messageService.CreateMessage(ctx, assistantMessage); err != nil { + c.Error(errors.NewInternalServerError(err.Error())) + return + } + + _, resp, err := h.sessionService.KnowledgeQAQuick(ctx, sessionID, request.Query) + if err != nil { + logger.ErrorWithFields(ctx, err, nil) + c.Error(errors.NewInternalServerError(err.Error())) + return + } + requestID := c.GetString(types.RequestIDContextKey.String()) + c.JSON(http.StatusOK, &types.QuickResponse{ + ID: requestID, + ResponseType: types.ResponseTypeAnswer, + Content: resp, + }) + +} + // completeAssistantMessage marks an assistant message as complete and updates it func (h *SessionHandler) completeAssistantMessage(ctx context.Context, assistantMessage *types.Message) { assistantMessage.UpdatedAt = time.Now() diff --git a/internal/models/embedding/ollama.go b/internal/models/embedding/ollama.go index ea545c5..715e1a5 100644 --- a/internal/models/embedding/ollama.go +++ b/internal/models/embedding/ollama.go @@ -5,9 +5,10 @@ import ( "fmt" "time" - ollamaapi "github.com/ollama/ollama/api" "knowlege-lsxd/internal/logger" "knowlege-lsxd/internal/models/utils/ollama" + + ollamaapi "github.com/ollama/ollama/api" ) // OllamaEmbedder implements text vectorization functionality using Ollama @@ -79,7 +80,14 @@ func (e *OllamaEmbedder) Embed(ctx context.Context, text string) ([]float32, err return embedding[0], nil } -// BatchEmbed converts multiple texts to vectors in batch +// BatchEmbed 为文本列表批量生成嵌入向量 +// 参数: +// - ctx: 上下文信息,用于控制请求的生命周期 +// - texts: 需要生成嵌入向量的文本列表 +// +// 返回值: +// - [][]float32: 生成的嵌入向量列表,每个文本对应一个向量 +// - error: 错误信息,如果成功则为nil func (e *OllamaEmbedder) BatchEmbed(ctx context.Context, texts []string) ([][]float32, error) { // Ensure model is available if err := e.ensureModelAvailable(ctx); err != nil { diff --git a/internal/models/utils/ollama/ollama.go b/internal/models/utils/ollama/ollama.go index ff824ff..8fe5e70 100644 --- a/internal/models/utils/ollama/ollama.go +++ b/internal/models/utils/ollama/ollama.go @@ -9,8 +9,9 @@ import ( "strings" "sync" - "github.com/ollama/ollama/api" "knowlege-lsxd/internal/logger" + + "github.com/ollama/ollama/api" ) // OllamaService manages Ollama service @@ -27,7 +28,7 @@ func GetOllamaService() (*OllamaService, error) { // Get Ollama base URL from environment variable, if not set use provided baseURL or default value logger.GetLogger(context.Background()).Infof("Ollama base URL: %s", os.Getenv("OLLAMA_BASE_URL")) baseURL := "http://localhost:11434" - envURL := os.Getenv("OLLAMA_BASE_URL") + envURL := "" //os.Getenv("OLLAMA_BASE_URL") if envURL != "" { baseURL = envURL } diff --git a/internal/router/router.go b/internal/router/router.go index 8fddce8..36b9236 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -66,7 +66,6 @@ func NewRouter(params RouterParams) *gin.Engine { // 初始化接口(不需要认证) r.GET("/api/v1/initialization/status", params.InitializationHandler.CheckStatus) r.GET("/api/v1/initialization/config", params.InitializationHandler.GetCurrentConfig) - r.POST("/api/v1/initialization/initialize", params.InitializationHandler.Initialize) // Ollama相关接口(不需要认证) r.GET("/api/v1/initialization/ollama/status", params.InitializationHandler.CheckOllamaStatus) @@ -151,6 +150,7 @@ func RegisterKnowledgeBaseRoutes(r *gin.RouterGroup, handler *handler.KnowledgeB // 知识库路由组 kb := r.Group("/knowledge-bases") { + kb.POST("/initialize", handler.InitSimple) // 创建知识库 kb.POST("", handler.CreateKnowledgeBase) // 获取知识库列表 @@ -200,6 +200,7 @@ func RegisterChatRoutes(r *gin.RouterGroup, handler *handler.SessionHandler) { knowledgeChat := r.Group("/knowledge-chat") { knowledgeChat.POST("/:session_id", handler.KnowledgeQA) + knowledgeChat.POST("/:session_id/quick", handler.KnowledgeQuick) } // 新增知识检索接口,不需要session_id @@ -214,6 +215,7 @@ func RegisterTenantRoutes(r *gin.RouterGroup, handler *handler.TenantHandler) { // 租户路由组 tenantRoutes := r.Group("/tenants") { + tenantRoutes.POST("", handler.CreateTenant) tenantRoutes.GET("/:id", handler.GetTenant) tenantRoutes.PUT("/:id", handler.UpdateTenant) diff --git a/internal/types/chat.go b/internal/types/chat.go index 3229cd3..cddb65e 100644 --- a/internal/types/chat.go +++ b/internal/types/chat.go @@ -43,6 +43,16 @@ type StreamResponse struct { KnowledgeReferences References `json:"knowledge_references"` } +// QuickResponse quick response +type QuickResponse struct { + // Unique identifier + ID string `json:"id"` + // Response type + ResponseType ResponseType `json:"response_type"` + // Current fragment content + Content string `json:"content"` +} + // References references type References []*SearchResult diff --git a/internal/types/interfaces/session.go b/internal/types/interfaces/session.go index 576708a..16cd064 100644 --- a/internal/types/interfaces/session.go +++ b/internal/types/interfaces/session.go @@ -26,6 +26,9 @@ type SessionService interface { KnowledgeQA(ctx context.Context, sessionID, query string, ) ([]*types.SearchResult, <-chan types.StreamResponse, error) + KnowledgeQAQuick(ctx context.Context, + sessionID, query string, + ) ([]*types.SearchResult, string, error) // KnowledgeQAByEvent performs knowledge-based question answering by event KnowledgeQAByEvent(ctx context.Context, chatManage *types.ChatManage, eventList []types.EventType) error // SearchKnowledge performs knowledge-based search, without summarization diff --git a/internal/types/knowledgebase.go b/internal/types/knowledgebase.go index 22b9eae..108f5af 100644 --- a/internal/types/knowledgebase.go +++ b/internal/types/knowledgebase.go @@ -46,6 +46,15 @@ type KnowledgeBase struct { DeletedAt gorm.DeletedAt `yaml:"deleted_at" json:"deleted_at" gorm:"index"` } +// initSimple +type InitSimpleRequest struct { + + // Name of the knowledge base + Name string `yaml:"name" json:"name"` + // Description of the knowledge base + Description string `yaml:"description" json:"description"` +} + // KnowledgeBaseConfig represents the knowledge base configuration type KnowledgeBaseConfig struct { // Chunking configuration diff --git a/internal/types/session.go b/internal/types/session.go index 7d209ed..40f2000 100644 --- a/internal/types/session.go +++ b/internal/types/session.go @@ -42,6 +42,8 @@ type SummaryConfig struct { Seed int `json:"seed"` // Max completion tokens MaxCompletionTokens int `json:"max_completion_tokens"` + // Think + Thinking *bool `json:"thinking"` } // Session represents the session