diff --git a/src/retk/const/response_codes.py b/src/retk/const/response_codes.py index 339cd63..5d78911 100644 --- a/src/retk/const/response_codes.py +++ b/src/retk/const/response_codes.py @@ -48,6 +48,7 @@ class CodeEnum(IntEnum): LLM_TIMEOUT = 38 LLM_SERVICE_ERROR = 39 LLM_NO_CHOICE = 40 + LLM_INVALID_RESPONSE_FORMAT = 41 @dataclass @@ -108,6 +109,7 @@ class CodeMessage: CodeEnum.LLM_TIMEOUT: CodeMessage(zh="模型超时", en="Model timeout"), CodeEnum.LLM_SERVICE_ERROR: CodeMessage(zh="模型服务错误", en="Model service error"), CodeEnum.LLM_NO_CHOICE: CodeMessage(zh="无回复", en="No response"), + CodeEnum.LLM_INVALID_RESPONSE_FORMAT: CodeMessage(zh="无效的回复格式", en="Invalid response format"), } CODE2STATUS_CODE: Dict[CodeEnum, int] = { @@ -152,6 +154,7 @@ class CodeMessage: CodeEnum.LLM_TIMEOUT: 408, CodeEnum.LLM_SERVICE_ERROR: 500, CodeEnum.LLM_NO_CHOICE: 404, + CodeEnum.LLM_INVALID_RESPONSE_FORMAT: 500, } diff --git a/src/retk/controllers/ai/knowledge.py b/src/retk/controllers/ai/knowledge.py index e50d80e..0b66ab4 100644 --- a/src/retk/controllers/ai/knowledge.py +++ b/src/retk/controllers/ai/knowledge.py @@ -43,3 +43,16 @@ async def accept_extended_node( requestId=au.request_id, node=get_node_data(n), ) + + +async def reject_extended_node( + au: AuthedUser, + eid: str, +) -> schemas.RequestIdResponse: + await core.ai.llm.knowledge.extended.reject_extended_node( + au=au, + eid=eid, + ) + return schemas.RequestIdResponse( + requestId=au.request_id, + ) diff --git a/src/retk/core/ai/llm/knowledge/__init__.py b/src/retk/core/ai/llm/knowledge/__init__.py index 9c81001..ffea231 100644 --- a/src/retk/core/ai/llm/knowledge/__init__.py +++ b/src/retk/core/ai/llm/knowledge/__init__.py @@ -1,54 +1,3 @@ -from pathlib import Path -from typing import Tuple - -from retk import const from . import extended from .extending import extend_on_node_update, extend_on_node_post, LLM_SERVICES -from ..api.base import BaseLLMService, MessagesType - -system_summary_prompt = (Path(__file__).parent / "system_summary.md").read_text(encoding="utf-8") -system_extend_prompt = (Path(__file__).parent / "system_extend.md").read_text(encoding="utf-8") - - -async def _send( - llm_service: BaseLLMService, - model: str, - system_prompt: str, - md: str, - req_id: str, -) -> Tuple[str, const.CodeEnum]: - _msgs: MessagesType = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": md}, - ] - return await llm_service.complete(messages=_msgs, model=model, req_id=req_id) - - -async def summary( - llm_service: BaseLLMService, - model: str, - md: str, - req_id: str = None, -) -> Tuple[str, const.CodeEnum]: - return await _send( - llm_service=llm_service, - model=model, - system_prompt=system_summary_prompt, - md=md, - req_id=req_id, - ) - - -async def extend( - llm_service: BaseLLMService, - model: str, - md: str, - req_id: str = None, -) -> Tuple[str, const.CodeEnum]: - return await _send( - llm_service=llm_service, - model=model, - system_prompt=system_extend_prompt, - md=md, - req_id=req_id, - ) +from .ops import summary, extend diff --git a/src/retk/core/ai/llm/knowledge/extended.py b/src/retk/core/ai/llm/knowledge/extended.py index 1b5630c..1907d2e 100644 --- a/src/retk/core/ai/llm/knowledge/extended.py +++ b/src/retk/core/ai/llm/knowledge/extended.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional from bson import ObjectId @@ -22,18 +22,21 @@ async def get_extended_nodes( async def accept_extended_node( au: AuthedUser, eid: str, -) -> Tuple[Node, CodeEnum]: +) -> Tuple[Optional[Node], CodeEnum]: if not is_local_db(): doc = await client.coll.llm_extended_node.find_one_and_delete( - {"_id": ObjectId(eid)}, + {"_id": ObjectId(eid), "uid": au.u.id}, ) else: doc = await client.coll.llm_extended_node.find_one( - {"_id": ObjectId(eid)}, - ) - await client.coll.llm_extended_node.delete_one( - {"_id": ObjectId(eid)}, + {"_id": ObjectId(eid), "uid": au.u.id}, ) + if doc is not None: + await client.coll.llm_extended_node.delete_one( + {"_id": ObjectId(eid), "uid": au.u.id}, + ) + if doc is None: + return None, CodeEnum.NODE_NOT_EXIST title = doc["sourceMd"].split("\n", 1)[0].strip() at_node = get_at_node_md_link(title, doc["sourceNid"]) md = doc["extendMd"] + "\n\n" + at_node @@ -43,3 +46,12 @@ async def accept_extended_node( from_nid=doc["sourceNid"], ) return n, code + + +async def reject_extended_node( + au: AuthedUser, + eid: str, +): + await client.coll.llm_extended_node.delete_one( + {"_id": ObjectId(eid), "uid": au.u.id}, + ) diff --git a/src/retk/core/ai/llm/knowledge/extending.py b/src/retk/core/ai/llm/knowledge/extending.py index f95e18f..486f339 100644 --- a/src/retk/core/ai/llm/knowledge/extending.py +++ b/src/retk/core/ai/llm/knowledge/extending.py @@ -28,10 +28,10 @@ async def extend_on_node_post(data: Node): uid=data["uid"], nid=data["id"], modifiedAt=int(data["modifiedAt"].replace(tzinfo=utc).timestamp()), - summaryService="tencent", - summaryModel=api.TencentModelEnum.HUNYUAN_LITE.value, - extendService="tencent", - extendModel=api.TencentModelEnum.HUNYUAN_LITE.value, + summaryService="baidu", + summaryModel=api.BaiduModelEnum.ERNIE_SPEED_8K.value, + extendService="moonshot", + extendModel=api.MoonshotModelEnum.V1_8K.value, ) # sort by _id desc diff --git a/src/retk/core/ai/llm/knowledge/ops.py b/src/retk/core/ai/llm/knowledge/ops.py new file mode 100644 index 0000000..d34fe65 --- /dev/null +++ b/src/retk/core/ai/llm/knowledge/ops.py @@ -0,0 +1,62 @@ +from pathlib import Path +from typing import Tuple + +from retk import const +from .utils import parse_json_pattern, remove_links +from ..api.base import BaseLLMService, MessagesType + +system_summary_prompt = (Path(__file__).parent / "system_summary.md").read_text(encoding="utf-8") +system_extend_prompt = (Path(__file__).parent / "system_extend.md").read_text(encoding="utf-8") + + +async def _send( + llm_service: BaseLLMService, + model: str, + system_prompt: str, + md: str, + req_id: str, +) -> Tuple[str, const.CodeEnum]: + _msgs: MessagesType = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": md}, + ] + return await llm_service.complete(messages=_msgs, model=model, req_id=req_id) + + +async def summary( + llm_service: BaseLLMService, + model: str, + md: str, + req_id: str = None, +) -> Tuple[str, const.CodeEnum]: + md_ = remove_links(md) + return await _send( + llm_service=llm_service, + model=model, + system_prompt=system_summary_prompt, + md=md_, + req_id=req_id, + ) + + +async def extend( + llm_service: BaseLLMService, + model: str, + md: str, + req_id: str = None, +) -> Tuple[str, const.CodeEnum]: + msg, code = await _send( + llm_service=llm_service, + model=model, + system_prompt=system_extend_prompt, + md=md, + req_id=req_id, + ) + if code != const.CodeEnum.OK: + return msg, code + + try: + title, content = parse_json_pattern(msg) + except ValueError as e: + return str(e), const.CodeEnum.LLM_INVALID_RESPONSE_FORMAT + return f"{title}\n\n{content}", const.CodeEnum.OK diff --git a/src/retk/core/ai/llm/knowledge/system_extend.md b/src/retk/core/ai/llm/knowledge/system_extend.md index 0b7c177..48a3ed9 100644 --- a/src/retk/core/ai/llm/knowledge/system_extend.md +++ b/src/retk/core/ai/llm/knowledge/system_extend.md @@ -1,25 +1,52 @@ 你是一个博学多才的人,拥有非常丰富的知识,而且会融会贯通不同知识, -也会对相似知识之间的关系做通熟易懂的类比。 +也会对相似知识之间的关系做通熟易懂的类比并延展到不同领域。 -接下来,我将展示我最近接触到的一些知识点, -请依据你的内在丰富的知识网络,帮我推荐出一条我会感兴趣的 **新知识**。 +接下来,我将展示我最近接触到的一条信息, +请依据你的内在丰富的知识网络,帮我推荐出一条我会感兴趣的 **新知识**,并以 json 格式返回结果。 -下面是一个推荐新知识的案例: +案例 1: -# 我展示的知识点 +# 我展示的信息 +""" 标题:幼儿因果关系认知的局限性 -知识点: +关键点: 1. **特点**:2-4岁的儿童由于脑部发育阶段的特性,无法推理长线的因果关系。 2. **原因**:这种现象的一个原因是前额叶的发展不足,无法模拟和推理未来发生的事情。 3. **长短期反馈**:他们无法理解一段时间后的结果,例如不吃饭会导致晚上肚子饿。尽管如此,他们可以理解短期反馈,例如挥手打人或者给脸色会有直接的结果。 + """ -# 你需要返回的新知识 +# 你需要返回的结果 -儿童发展中的同理心培养 +{ +"title": "儿童发展中的同理心培养", +"content": "- 富有同理心的小孩能理解和感受他人情感,有助于儿童建立良好的人际关系和社交技巧。\n- +儿童的同理心发展分为不同阶段,从2岁开始,他们能够感知到他人的情感,而4-5岁时,他们开始能够理解他人的观点和需求。\n- +家长和教育者可以通过共情、角色扮演、讲述故事、以及引导儿童关注他人的感受等方法,帮助儿童培养同理心。" +} -- 富有同理心的小孩能理解和感受他人情感,有助于儿童建立良好的人际关系和社交技巧。 -- 儿童的同理心发展分为不同阶段,从2岁开始,他们能够感知到他人的情感,而4-5岁时,他们开始能够理解他人的观点和需求。 -- 家长和教育者可以通过共情、角色扮演、讲述故事、以及引导儿童关注他人的感受等方法,帮助儿童培养同理心。 +案例 2: + +# 我展示的信息 + +""" +标题:水的分子结构及其在生命中的重要性 + +关键点: + +1. 水是生命的基础 +2. 水分子结构:H2O,氢氧共价键 +3. 水的偏电性:氢正电荷,氧负电荷 +4. 水作为良好溶剂:吸附其他分子,如盐 +5. 生命过程中水的作用:输送养分和排除废物 + """ + +# 你需要返回的结果 + +{ +"title": "水的凝聚力和表面张力现象", +"content": " +凝聚力使水分子紧密相连,表面张力导致水成球状以减小表面积。这些现象在植物水分运输、清洁剂使用和雨伞设计等方面具有重要作用。通过探讨这些现象,可以更深入地理解水的特性及其在自然和生活中的应用。" +} diff --git a/src/retk/core/ai/llm/knowledge/system_summary.md b/src/retk/core/ai/llm/knowledge/system_summary.md index 94e26b1..24cfbe6 100644 --- a/src/retk/core/ai/llm/knowledge/system_summary.md +++ b/src/retk/core/ai/llm/knowledge/system_summary.md @@ -1,23 +1,50 @@ 你是一个博学多才的人,拥有非常丰富的知识,十分善于用简练的语言总结复杂的概念。 -接下来,我将展示我最近接触到的一些知识和信息,请帮我提炼总结这段认知的关键信息,总结一个简短标题,并简短罗列出知识点来。 +接下来,我将展示我最近接触到的一些知识或信息,请帮我提炼总结这段认知的关键信息,总结一个简短标题,并简短罗列出知识点来。 -比如下面的这个例子: +案例 1: -# 我展示的信息 +# 我展示的信息: +""" 小孩建立长线的因果关系 - 因为脑部发育阶段的特性,2-4岁的儿童没办法推理比较长线的因果关系,比如不吃饭,晚上会肚子饿。其中的一个原因是前额叶的发展不够,没办法模拟和推理未来发生的事情,也就没办法思考一段时间后的结果。 - 但是短期反馈还是有的,比如挥手要打人或者给脸色的时候能有直接的映射结果,这点他们理解 +""" -# 你需要返回的总结格式 +# 你需要返回的总结格式: +""" 标题:幼儿因果关系认知的局限性 -知识点: +关键点: 1. **特点**:2-4岁的儿童由于脑部发育阶段的特性,无法推理长线的因果关系。 2. **原因**:这种现象的一个原因是前额叶的发展不足,无法模拟和推理未来发生的事情。 3. **长短期反馈**:他们无法理解一段时间后的结果,例如不吃饭会导致晚上肚子饿。尽管如此,他们可以理解短期反馈,例如挥手打人或者给脸色会有直接的结果。 + """ + +案例 2: + +# 我展示的信息: + +""" +水是原子层面生命的基础 +水 H2O ,氢原子分享了一个电子给氧,达成了稳固态。这种结合让氢这边带一点正电荷,氧这边带点负电荷 +它可以用这种特性吸附是他分子,成为很好的容器 +可以想象成水把其它分子拆散的过程,比如盐溶于水。这让水可以很容易将生命需要的各种养料和废物输入输出。所以是生命形成的关键因素。 +""" + +# 你需要返回的总结格式: + +""" +标题:水的分子结构及其在生命中的重要性 + +关键点: + +1. 水是生命的基础 +2. 水分子结构:H2O,氢氧共价键 +3. 水的偏电性:氢正电荷,氧负电荷 +4. 水作为良好溶剂:吸附其他分子,如盐 +5. 生命过程中水的作用:输送养分和排除废物 + """ diff --git a/src/retk/core/ai/llm/knowledge/utils.py b/src/retk/core/ai/llm/knowledge/utils.py new file mode 100644 index 0000000..70d2aa0 --- /dev/null +++ b/src/retk/core/ai/llm/knowledge/utils.py @@ -0,0 +1,22 @@ +import json +import re +from typing import Tuple + +JSON_PTN = re.compile(r"^({\s*?\"title\":\s?\".+?\",\s*?\"content\":\s?\".+?\"\s*?})", re.DOTALL | re.MULTILINE) +IMG_PTN = re.compile(r"!\[.*?\]\(.+?\)") +LINK_PTN = re.compile(r"\[(.*?)]\(.+?\)") + + +def parse_json_pattern(text: str) -> Tuple[str, str]: + m = JSON_PTN.search(text) + if m: + json_str = m.group(1) + d = json.loads(json_str) + return d["title"], d["content"] + raise ValueError(f"Invalid JSON pattern: {text}") + + +def remove_links(text: str) -> str: + t_ = IMG_PTN.sub("", text) + t_ = LINK_PTN.sub(r"\1", t_) + return t_ diff --git a/src/retk/core/scheduler/schedule.py b/src/retk/core/scheduler/schedule.py index acd866d..ef2f03f 100644 --- a/src/retk/core/scheduler/schedule.py +++ b/src/retk/core/scheduler/schedule.py @@ -97,7 +97,7 @@ def init_tasks(): run_every_at( job_id="deliver_unscheduled_node_extend", func=tasks.extend_node.deliver_unscheduled_extend_nodes, - minute=0, + second=0, ) return diff --git a/src/retk/core/scheduler/tasks/extend_node.py b/src/retk/core/scheduler/tasks/extend_node.py index 9386fcd..4a50e34 100644 --- a/src/retk/core/scheduler/tasks/extend_node.py +++ b/src/retk/core/scheduler/tasks/extend_node.py @@ -47,7 +47,7 @@ async def async_deliver_unscheduled_extend_nodes() -> str: if code != const.CodeEnum.OK: logger.error(f"knowledge summary error: {code}") continue - oneline_s = _summary.replace('\n', '\n\n') + oneline_s = _summary.replace('\n', '\\n') logger.debug(f"summary: {oneline_s}") e0 = time.perf_counter() _extended, code = await knowledge.extend( @@ -60,7 +60,7 @@ async def async_deliver_unscheduled_extend_nodes() -> str: if code != const.CodeEnum.OK: logger.error(f"knowledge extend error: {code}") continue - oneline_e = _extended.replace('\n', '\n\n') + oneline_e = _extended.replace('\n', '\\n') logger.debug(f"extended: {oneline_e}") ext = ExtendedNode( uid=item["uid"], diff --git a/src/retk/routes/ai.py b/src/retk/routes/ai.py index 6fe198d..76ed356 100644 --- a/src/retk/routes/ai.py +++ b/src/retk/routes/ai.py @@ -29,7 +29,7 @@ async def get_extended_nodes( @router.post( - path="/extended-nodes/accept/{eid}", + path="/extended-nodes/{eid}", status_code=201, response_model=schemas.node.NodeResponse, ) @@ -42,3 +42,19 @@ async def accept_extended_node( au=au, eid=eid, ) + + +@router.delete( + path="/extended-nodes/{eid}", + status_code=200, + response_model=schemas.RequestIdResponse, +) +async def reject_extended_node( + au: utils.ANNOTATED_AUTHED_USER, + eid: str, + referer: Optional[str] = utils.DEPENDS_REFERER, +) -> schemas.RequestIdResponse: + return await knowledge.reject_extended_node( + au=au, + eid=eid, + ) diff --git a/tests/test_ai_llm_knowledge.py b/tests/test_ai_llm_knowledge.py index 73fe750..a70462a 100644 --- a/tests/test_ai_llm_knowledge.py +++ b/tests/test_ai_llm_knowledge.py @@ -1,38 +1,59 @@ import unittest +from textwrap import dedent from retk import const from retk.core.ai import llm +from retk.core.ai.llm.knowledge.utils import parse_json_pattern from . import utils from .test_ai_llm_api import skip_no_api_key, clear_all_api_key -md_source = """\ -广东猪脚饭特点 - -广州猪脚饭超越沙县小吃兰州拉面等,成为广东第一中式快餐。 - -原因是 - -1. 广东人口分布有很多外来人口,猪脚饭兼容了很多口味 -2. 工艺简单,大量的预制工作,较低出餐时间,出餐快。适合快节奏的打工人群 -3. 因为出餐快,所以不用招人,省人力成本 - -![IMG6992.png](https://files.rethink.run/userData/3a4344ccd6ba477e59ddf1f7f67e98bd.png) - -更值得一提的是猪脚饭在广东便宜,其它地方贵,原因之一是可以从香港走私猪脚,因为外国人不吃,所以产能过剩 - -【猪脚饭如何成为广东的快餐之王?【食录】-哔哩哔哩】 https://b23.tv/YUlg1nN -""" - -md_summary = """\ -标题:广东猪脚饭的快餐特色与成功因素 - -知识点: -1. **市场接受度**:广东猪脚饭因兼容多种口味,受到广泛欢迎,超越沙县小吃和兰州拉面成为广东最受欢迎的中式快餐。 -2. **人口结构**:广东的外来人口众多,猪脚饭满足了不同地域人群的口味需求。 -3. **工艺优势**:猪脚饭的制作工艺简单,预制工作量大,出餐速度快,适合快节奏生活。 -4. **成本效益**:快速出餐减少了人力成本,提高了经营效率。 -5. **价格因素**:猪脚饭在广东价格低廉,部分原因是可能通过香港走私猪脚,利用外国人不吃猪脚导致的产能过剩。 -""" +md_source = ["""\ + 广东猪脚饭特点 + + 广州猪脚饭超越沙县小吃兰州拉面等,成为广东第一中式快餐。 + + 原因是 + + 1. 广东人口分布有很多外来人口,猪脚饭兼容了很多口味 + 2. 工艺简单,大量的预制工作,较低出餐时间,出餐快。适合快节奏的打工人群 + 3. 因为出餐快,所以不用招人,省人力成本 + + ![IMG6992.png](https://files.rethink.run/userData/3a4344ccd6ba477e59ddf1f7f67e98bd.png) + + 更值得一提的是猪脚饭在广东便宜,其它地方贵,原因之一是可以从香港走私猪脚,因为外国人不吃,所以产能过剩 + + 【猪脚饭如何成为广东的快餐之王?【食录】-哔哩哔哩】 https://b23.tv/YUlg1nN + """, + """\ + 蓝莓散射光表现蓝色 + + 蓝莓是为数不多的蓝色水果。如果用化学的方法合成蓝色,那太费成本了。所以它选择用物理散射, + [@天空为什么是蓝色](/n/4dmEv7TLvGHdPymmWtoAvhQH) 蓝天的方法,用表层的一层石蜡空心结构, + 将蓝色散射出来。同时鸟类视觉对蓝色敏感,他们更能看到蓝色 + """ + ] + +md_summary = [ + """\ + 标题:广东猪脚饭的快餐特色与成功因素 + + 知识点: + 1. **市场接受度**:广东猪脚饭因兼容多种口味,受到广泛欢迎,超越沙县小吃和兰州拉面成为广东最受欢迎的中式快餐。 + 2. **人口结构**:广东的外来人口众多,猪脚饭满足了不同地域人群的口味需求。 + 3. **工艺优势**:猪脚饭的制作工艺简单,预制工作量大,出餐速度快,适合快节奏生活。 + 4. **成本效益**:快速出餐减少了人力成本,提高了经营效率。 + 5. **价格因素**:猪脚饭在广东价格低廉,部分原因是可能通过香港走私猪脚,利用外国人不吃猪脚导致的产能过剩。 + """, + """\ + 标题:蓝莓的蓝色散射原理及其生态意义 + + 关键点: + 1. 蓝莓呈现蓝色的独特性:是少数天然蓝色水果之一。 + 2. 物理散射机制:通过表层的石蜡空心结构实现蓝色光的散射。 + 3. 经济性:物理散射相比化学合成更为经济。 + 4. 生态适应性:鸟类视觉对蓝色敏感,有助于蓝莓种子的传播。 + """ +] class LLMKnowledgeExtendTest(unittest.IsolatedAsyncioTestCase): @@ -53,34 +74,94 @@ def tearDown(self): async def test_summary(self): for service, model in [ (llm.api.TencentService(), llm.api.TencentModelEnum.HUNYUAN_LITE), - (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN1_5_05B), + (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN_2B), (llm.api.BaiduService(), llm.api.BaiduModelEnum.ERNIE_SPEED_8K), # (llm.api.OpenaiService(), llm.api.OpenaiModelEnum.GPT4), (llm.api.XfYunService(), llm.api.XfYunModelEnum.SPARK_LITE), (llm.api.MoonshotService(), llm.api.MoonshotModelEnum.V1_8K), # 这个总结比较好 ]: - text, code = await llm.knowledge.summary( - llm_service=service, - model=model.value, - md=md_source, - ) - self.assertEqual(const.CodeEnum.OK, code, msg=text) - print(f"{service.__class__.__name__} {model.name}\n{text}\n\n") + for md in md_source: + text, code = await llm.knowledge.summary( + llm_service=service, + model=model.value, + md=md, + ) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + print(f"{service.__class__.__name__} {model.name}\n{text}\n\n") @skip_no_api_key async def test_extend(self): for service, model in [ # (llm.api.TencentService(), llm.api.TencentModelEnum.HUNYUAN_PRO), - # (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN_PLUS), + (llm.api.TencentService(), llm.api.TencentModelEnum.HUNYUAN_STANDARD), + (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN_PLUS), (llm.api.BaiduService(), llm.api.BaiduModelEnum.ERNIE35_8K), # (llm.api.OpenaiService(), llm.api.OpenaiModelEnum.GPT4), - # (llm.api.XfYunService(), llm.api.XfYunModelEnum.SPARK_PRO), - # (llm.api.MoonshotService(), llm.api.MoonshotModelEnum.V1_8K), # 这个延伸比较好 + (llm.api.XfYunService(), llm.api.XfYunModelEnum.SPARK_PRO), + (llm.api.MoonshotService(), llm.api.MoonshotModelEnum.V1_8K), # 这个延伸比较好 ]: - text, code = await llm.knowledge.extend( - llm_service=service, - model=model.value, - md=md_summary, - ) - self.assertEqual(const.CodeEnum.OK, code, msg=text) - print(f"{service.__class__.__name__} {model.name}\n{text}\n\n") + for md in md_source: + text, code = await llm.knowledge.extend( + llm_service=service, + model=model.value, + md=md, + ) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + print(f"{service.__class__.__name__} {model.name}\n{text}\n\n") + + def test_json_pattern(self): + cases = [ + """\ + { + "title": "tttt", + "content": "cccc" + } + """, + """{"title":"tttt","content":"cccc"}""", + """\ + 1afwenq 是当前 + 轻ww 1 + { + "title": "tttt", + "content": "cccc" + } + """, + """\ + { + "title": "tttt", + "content": "cccc" + } + 23423saq1是当前 + """ + ] + for case in cases: + case = dedent(case) + title, content = parse_json_pattern(case) + self.assertEqual("tttt", title) + self.assertEqual("cccc", content) + + bad_cases = [ + """\ + { + 'title': "tttt", + "content": "cccc" + } + """, + """\ + { + "title": 'tttt', + "content": "cccc" + } + """, + """\ + { + "t1itle": "tttt", + "content": "cccc" + } + """ + ] + + for case in bad_cases: + case = dedent(case) + with self.assertRaises(ValueError): + parse_json_pattern(case) diff --git a/tests/test_api.py b/tests/test_api.py index fef2319..9a469f8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1603,7 +1603,7 @@ async def test_node_extend(self): self.assertEqual(node["id"], n["sourceNid"]) resp = self.client.post( - f"/api/ai/extended-nodes/accept/{n['id']}", + f"/api/ai/extended-nodes/{n['id']}", headers=self.default_headers, ) rj = self.check_ok_response(resp, 201) @@ -1618,3 +1618,27 @@ async def test_node_extend(self): ) rj = self.check_ok_response(resp, 200) self.assertEqual(0, len(rj["nodes"])) + + oid = ObjectId() + await client.coll.llm_extended_node.insert_one( + ExtendedNode( + _id=oid, + uid=uid, + sourceNid=node["id"], + sourceMd=node["md"], + extendMd="this is extended md", + ), + ) + + resp = self.client.delete( + f"/api/ai/extended-nodes/{oid}", + headers=self.default_headers, + ) + self.check_ok_response(resp, 200) + + resp = self.client.get( + f"/api/ai/extended-nodes", + headers=self.default_headers, + ) + rj = self.check_ok_response(resp, 200) + self.assertEqual(0, len(rj["nodes"])) diff --git a/tests/test_core_local.py b/tests/test_core_local.py index 6c32ab0..2cfac8d 100644 --- a/tests/test_core_local.py +++ b/tests/test_core_local.py @@ -27,7 +27,7 @@ from . import utils -@patch("retk.core.ai.llm.knowledge._send", new_callable=AsyncMock, return_value=["", const.CodeEnum.OK]) +@patch("retk.core.ai.llm.knowledge.ops._send", new_callable=AsyncMock, return_value=["", const.CodeEnum.OK]) class LocalModelsTest(unittest.IsolatedAsyncioTestCase): @classmethod def setUpClass(cls) -> None: diff --git a/tests/test_core_remote.py b/tests/test_core_remote.py index f364ebf..7566d0c 100644 --- a/tests/test_core_remote.py +++ b/tests/test_core_remote.py @@ -21,7 +21,7 @@ from . import utils -@patch("retk.core.ai.llm.knowledge._send", new_callable=AsyncMock, return_value=["", const.CodeEnum.OK]) +@patch("retk.core.ai.llm.knowledge.ops._send", new_callable=AsyncMock, return_value=["", const.CodeEnum.OK]) class RemoteModelsTest(unittest.IsolatedAsyncioTestCase): default_pwd = "rethink123"