From f0d238690fd68a5b8087109a8fffa7dc913e89e7 Mon Sep 17 00:00:00 2001 From: zc <2649023789@qq.com> Date: Thu, 2 Jul 2026 10:29:14 +0800 Subject: [PATCH] Initial project import --- .gitignore | 33 + 1.html | 138 + Prompt.py | 66 + README.md | 0 agent_create_request.py | 361 ++ agent_ids.txt | 8 + agent_role_request.py | 156 + app.py | 1127 ++++ audio/test.wav | Bin 0 -> 13615182 bytes checkpointer_config.py | 498 ++ config | 0 main_agent.py | 247 + modelsAPI/__init__.py | 0 modelsAPI/model_api | 0 modelsAPI/model_api.py | 386 ++ picture/015ebb33d2a3_20260324072409.png | Bin 0 -> 10626 bytes picture/08f8efd6f1d4_20260324110619.png | Bin 0 -> 10626 bytes picture/1ee23229170a_20260324082344.png | Bin 0 -> 10626 bytes picture/22f06245ed56_20260601022623.png | Bin 0 -> 1047168 bytes picture/334ee511347b_20260323061707.png | Bin 0 -> 1198166 bytes picture/4341.png | Bin 0 -> 247350 bytes picture/44b07d453030_20260323024633.png | Bin 0 -> 1198166 bytes picture/50da933dcb11_20260323062013.jpg | Bin 0 -> 45976 bytes picture/58a85f24f7cf_20260323024053.png | Bin 0 -> 15070 bytes picture/79493c5be2ac_20260324081251.png | Bin 0 -> 10626 bytes picture/95f8928fb88c_20260324083528.png | Bin 0 -> 10626 bytes picture/OIP-C.png | Bin 0 -> 10626 bytes picture/bbeb197ac231_20260323060515.png | Bin 0 -> 1198166 bytes picture/d345fbf33f95_20260325031818.png | Bin 0 -> 10626 bytes picture/dcd6a33ef2b0_20260324071427.png | Bin 0 -> 10626 bytes picture/e0bb42e29c58_20260323062055.jpg | Bin 0 -> 45976 bytes picture/f233ccc07264_20260323060328.png | Bin 0 -> 1198166 bytes picture/f38267265a8a_20260323032735.png | Bin 0 -> 1864 bytes picture/image_records.json | 98 + prompts.py | 1243 +++++ requirements.txt | 44 + template.tex | 47 + tools/__init__.py | 41 + tools/agent_usage_statistics.py | 237 + tools/fault_record_db.py | 330 ++ tools/fault_statistics.py | 969 ++++ tools/function_tool.py | 253 + tools/graph_tools.py | 1792 +++++++ tools/pdf_generator.py | 182 + tools/rag_tools.py | 274 + tools/ship_model_db.py | 200 + tools/test.py | 251 + tools/text2sql_tool.py | 776 +++ tools/texttosql_utils.py | 0 tools/vlm_tools.py | 298 ++ utils/MML2OMML.XSL | 4728 +++++++++++++++++ utils/__init__.py | 4 + utils/function_tracker.py | 360 ++ utils/image_utils.py | 201 + utils/intent_matcher.py | 73 + utils/pdf_generator.py | 280 + utils/ship_number_search.py | 463 ++ utils/template.tex | 47 + utils/word_generator.py | 790 +++ workflow_registry.py | 158 + workflows/__init__.py | 20 + workflows/history_manager.py | 217 + workflows/workflow_baike.py | 593 +++ workflows/workflow_fault_diagnosis.py | 1691 ++++++ workflows/workflow_fault_diagnosis.py智能检索 | 1665 ++++++ workflows/workflow_operate.py | 1470 +++++ workflows/workflow_operate.py智能检索 | 1470 +++++ workflows/workflow_operate624.py | 1469 +++++ workflows/workflow_other.py | 293 + workflows/workflow_other_report.py | 572 ++ workflows/workflow_qa_report.py | 544 ++ workflows/workflow_statistics.py | 484 ++ workflows/workflow_utils.py | 169 + 73 files changed, 27816 insertions(+) create mode 100644 .gitignore create mode 100644 1.html create mode 100644 Prompt.py create mode 100644 README.md create mode 100644 agent_create_request.py create mode 100644 agent_ids.txt create mode 100644 agent_role_request.py create mode 100644 app.py create mode 100644 audio/test.wav create mode 100644 checkpointer_config.py create mode 100644 config create mode 100644 main_agent.py create mode 100644 modelsAPI/__init__.py create mode 100644 modelsAPI/model_api create mode 100644 modelsAPI/model_api.py create mode 100644 picture/015ebb33d2a3_20260324072409.png create mode 100644 picture/08f8efd6f1d4_20260324110619.png create mode 100644 picture/1ee23229170a_20260324082344.png create mode 100644 picture/22f06245ed56_20260601022623.png create mode 100644 picture/334ee511347b_20260323061707.png create mode 100644 picture/4341.png create mode 100644 picture/44b07d453030_20260323024633.png create mode 100644 picture/50da933dcb11_20260323062013.jpg create mode 100644 picture/58a85f24f7cf_20260323024053.png create mode 100644 picture/79493c5be2ac_20260324081251.png create mode 100644 picture/95f8928fb88c_20260324083528.png create mode 100644 picture/OIP-C.png create mode 100644 picture/bbeb197ac231_20260323060515.png create mode 100644 picture/d345fbf33f95_20260325031818.png create mode 100644 picture/dcd6a33ef2b0_20260324071427.png create mode 100644 picture/e0bb42e29c58_20260323062055.jpg create mode 100644 picture/f233ccc07264_20260323060328.png create mode 100644 picture/f38267265a8a_20260323032735.png create mode 100644 picture/image_records.json create mode 100644 prompts.py create mode 100644 requirements.txt create mode 100644 template.tex create mode 100644 tools/__init__.py create mode 100644 tools/agent_usage_statistics.py create mode 100644 tools/fault_record_db.py create mode 100644 tools/fault_statistics.py create mode 100644 tools/function_tool.py create mode 100644 tools/graph_tools.py create mode 100644 tools/pdf_generator.py create mode 100644 tools/rag_tools.py create mode 100644 tools/ship_model_db.py create mode 100644 tools/test.py create mode 100644 tools/text2sql_tool.py create mode 100644 tools/texttosql_utils.py create mode 100644 tools/vlm_tools.py create mode 100644 utils/MML2OMML.XSL create mode 100644 utils/__init__.py create mode 100644 utils/function_tracker.py create mode 100644 utils/image_utils.py create mode 100644 utils/intent_matcher.py create mode 100644 utils/pdf_generator.py create mode 100644 utils/ship_number_search.py create mode 100644 utils/template.tex create mode 100644 utils/word_generator.py create mode 100644 workflow_registry.py create mode 100644 workflows/__init__.py create mode 100644 workflows/history_manager.py create mode 100644 workflows/workflow_baike.py create mode 100644 workflows/workflow_fault_diagnosis.py create mode 100644 workflows/workflow_fault_diagnosis.py智能检索 create mode 100644 workflows/workflow_operate.py create mode 100644 workflows/workflow_operate.py智能检索 create mode 100644 workflows/workflow_operate624.py create mode 100644 workflows/workflow_other.py create mode 100644 workflows/workflow_other_report.py create mode 100644 workflows/workflow_qa_report.py create mode 100644 workflows/workflow_statistics.py create mode 100644 workflows/workflow_utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18b26e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +__pycache__/ +*.py[cod] +*$py.class + +.Python +.venv/ +venv/ +env/ +ENV/ + +*.swp +*.bak* +*.before_rollback_* +*.tmp +*.log + +config.py +config.txt + +data/*.db +data/*.db-* +data/*.sqlite +data/*.sqlite-* + +created_pdf/ +created_word/ +image_output/ + +workflows/=* + +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ diff --git a/1.html b/1.html new file mode 100644 index 0000000..311b573 --- /dev/null +++ b/1.html @@ -0,0 +1,138 @@ +
根据离心式海水泵近一年(2023年1月至2023年12月)的备件消耗统计数据显示,系统运行期间未记录到任何故障事件(total_events=0),相关备件消耗数据及故障频率数据均为空。初步判断可能为数据采集系统未启用、设备运行周期未覆盖统计时段或设备处于低负荷运行状态。建议进一步核查数据采集机制完整性,并结合设备实际运行工况开展专项分析。
+ +| 备件名称 | +库存数量 | +安全库存阈值 | +库存状态 | +
|---|---|---|---|
| 泵轴密封圈 | +0 | +5 | +告警 | +
| 叶轮组件 | +2 | +3 | +预警 | +
| 轴承组(6308型号) | +8 | +10 | +正常 | +
| 故障类型 | +发生次数 | +占比(%) | +
|---|---|---|
| 无记录 | +0 | +0 | +
注:当前统计时段内未发生可记录故障事件,建议结合设备巡检日志开展隐性故障排查。
+ +| 统计时段 | +泵轴密封圈消耗量 | +叶轮组件消耗量 | +轴承组消耗量 | +
|---|---|---|---|
| 2023Q1 | +0 | +0 | +0 | +
| 2023Q2 | +0 | +0 | +0 | +
| 2023Q3 | +0 | +0 | +0 | +
| 2023Q4 | +0 | +0 | +0 | +
趋势说明:因无实际消耗数据,无法绘制趋势曲线。建议结合设备运行日志补充关键工况参数(如启停频次、海水含沙量等)开展关联分析。
+ +| 预测维度 | +预测周期 | +预测结论 | +
|---|---|---|
| 备件需求量 | +2024年全年 | +基于当前零消耗数据,预测需求量为0(需结合设备启用计划修正) | +
| 故障概率 | +2024年Q1-Q4 | +因无历史故障数据,无法进行概率建模,建议开展设备健康度评估 | +
结论:当前数据缺失可能导致保障策略失准,建议尽快启动设备运行数据采集与历史故障档案重建工作。
\ No newline at end of file diff --git a/Prompt.py b/Prompt.py new file mode 100644 index 0000000..66ca160 --- /dev/null +++ b/Prompt.py @@ -0,0 +1,66 @@ +from openai import OpenAI +query_type_prompt = """ +你是一个专业的装备维修领域用户查询判断助手,能够根据以下规则准确判断用户问题属于哪种类型。 + +### 判断规则 +- **简单问答类型(simple)**:问题属于日常生活常识、通用知识、非技术性提问,**不涉及**装备、设备、机械、电子、维修、故障诊断、零部件、操作流程等专业领域内容。例如问候、自我介绍、天气、时间等。 +- **复杂问答类型(complex)**:问题**涉及**装备、设备、机械系统(如发动机、液压系统)、电子装置、维修流程、故障排查、零部件组成、技术参数、操作规范等专业领域,即使只是询问定义、组成或功能,也视为 complex。 + +### 输出要求 +- 仅输出 JSON 格式:{"query_type":"simple"} 或 {"query_type":"complex"} +- 不得包含任何其他文字、解释或格式。 + +### 示例 +输入:今天心情怎么样 +输出:{"query_type":"simple"} + +输入:请问你是谁 +输出:{"query_type":"simple"} + +输入:发动机是什么 +输出:{"query_type":"complex"} + +输入:缸盖组包含哪些组件 +输出:{"query_type":"complex"} + +输入:手机屏幕碎了怎么办 +输出:{"query_type":"complex"} + +输入:水的沸点是多少 +输出:{"query_type":"simple"} + +输入:如何更换坦克履带 +输出:{"query_type":"complex"} + +### 用户查询: +text +""" +def open_api_chat(query: str, model: str = None): + """ + 同步调用Chat API + + Args: + query: 查询文本 + model: 模型名称(可选,默认使用环境变量OPENAI_MODEL) + """ + if model is None: + model = "Qwen3.5-35B-A3B" + response = OpenAI( + api_key = "none", + base_url = "http://192.168.0.46:59800/v1" + ).chat.completions.create( + model=model, + messages=[ + {"role": "user", "content": query}, + ], + temperature=0.6, + stream=False, + response_format = {"type": "json_object"}, + + ) + return response.choices[0].message.content +# querycontent = "发动机异响" +# query = query_type_prompt.replace("text",querycontent) + +# res = open_api_chat(query=query) +# print(res) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/agent_create_request.py b/agent_create_request.py new file mode 100644 index 0000000..bc657d4 --- /dev/null +++ b/agent_create_request.py @@ -0,0 +1,361 @@ +# create_agents.py +import requests +import urllib3 +from uuid import uuid4 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BASE_URL = "http://192.168.0.46:22000" +AGENT_API = f"{BASE_URL}/api/v1/agents/" +HEADERS = { + "Accept": "application/json", + "Content-Type": "application/json", +} + +TIMEOUT = 10 +OUTPUT_FILE = "agent_ids.txt" + +AGENT_META = { + str(uuid4()): { + "icon_index": 1, + "name": "修理工程管理", + "description": "对舰船修理工程实施全过程管理,包括任务分解、进度控制、工序协调和资源调配。", + "prompt": "助理员您好,我是修理工程管理助手,我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。我可以为您提供舰船设备的标准修理内容范围、作业工序清单以及安全施工要求,请直接告诉我您需要查询的设备名称。", + "default_query": [ + {"title": "查询设备标准 →", "content": "查询设备标准"}, + {"title": "查看作业工序 →", "content": "查看作业工序"} + ], + }, + str(uuid4()): { + "icon_index": 2, + "name": "器材统计与分析", + "description": "对舰船维修保障过程中涉及的器材进行统计分析与趋势研判。", + "prompt": "助理员您好,我是器材统计与分析助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。为您提供多维度的器材消耗统计与库存分析服务。请告诉我您需要统计的具体业务场景。", + "default_query": [ + {"title": "统计第一季度器材消耗 →", "content": "统计第一季度器材消耗"}, + {"title": "分析当前库存周转率 →", "content": "分析当前库存周转率"}, + {"title": "查看特定型号历史记录 →", "content": "查看特定型号历史记录"} + ], + }, + str(uuid4()): { + "icon_index": 3, + "name": "修理法规知识查询", + "description": "提供舰船修理相关法规、标准和制度的查询与解读。", + "prompt": "您好!我是修理法规知识查询助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。请告诉我需要查询的主题。", + "default_query": [ + {"title": "查询焊接工艺规程 →", "content": "查询焊接工艺规程"}, + {"title": "查看设备拆装安全规程 →", "content": "查看设备拆装安全规程"}, + {"title": "搜索环保作业规定 →", "content": "搜索环保作业规定"} + ], + }, + str(uuid4()): { + "icon_index": 4, + "name": "船舰知识查询", + "description": "提供舰船结构、系统组成与功能原理等知识。", + "prompt": "您好!我是舰船知识查询助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。请告诉我需要了解的主题。", + "default_query": [ + {"title": "了解舰船电力系统组成 →", "content": "了解舰船电力系统组成"}, + {"title": "查询舰船推进装置类型 →", "content": "查询舰船推进装置类型"}, + {"title": "搜索舰船雷达系统原理 →", "content": "搜索舰船雷达系统原理"} + ], + }, + str(uuid4()): { + "icon_index": 5, + "name": "故障排查与修理", + "description": "辅助进行舰船设备故障分析、原因定位与修理方案制定。", + "prompt": "您好!我是智能故障排查与修理助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。提供舷号、设备名称和故障现象,我可以协助您进行复杂故障的研判诊断。", + "default_query": [ + {"title": "多模态输出与校验 →", "content": "多模态输出与校验"}, + {"title": "诊断研判与可能性排序 →", "content": "诊断研判与可能性排序"}, + {"title": "GT-25000燃气轮机的日常维护保养方案 →", "content": "GT-25000燃气轮机的日常维护保养方案"} + ], + }, + str(uuid4()): { + "icon_index": 6, + "name": "舰船机电知识", + "description": "提供舰船动力、电气、液压等机电系统相关知识。", + "prompt": "机电长您好,我是舰船机电知识查询助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。请告诉我需了解的主题。", + "default_query": [ + {"title": "了解主推进系统构成 →", "content": "了解主推进系统构成"}, + {"title": "查看舰艇液压机参数 →", "content": "查看舰艇液压机参数"}, + {"title": "查看舰机液压系统参数 →", "content": "查看舰机液压系统参数"} + ], + }, + str(uuid4()): { + "icon_index": 7, + "name": "现场维修作业向导", + "description": "为一线维修人员提供现场操作流程与安全指导。", + "prompt": "您好!我是现场维修作业向导。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。您可以询问具体的维修步骤、拆装工艺或查询安全操作规程。", + "default_query": [ + {"title": "离心泵汽蚀的典型特征 →", "content": "离心泵汽蚀的典型特征"}, + {"title": "海水泵汽蚀的维修步骤 →", "content": "海水泵汽蚀的维修步骤"}, + {"title": "GT-25000燃气轮机的日常维护步骤 →", "content": "GT-25000燃气轮机的日常维护步骤"} + ], + }, + str(uuid4()): { + "icon_index": 8, + "name": "舰船技术知识百科", + "description": "综合性舰船技术知识库。", + "prompt": "您好,我是舰船技术知识百科助手。我已经绑定了技术资料、通用知识、故障知识、法规标准知识库。请告诉我您需要查询的技术主题。", + "default_query": [ + {"title": "了解船体高强钢性能 →", "content": "了解船体高强钢性能"}, + {"title": "查询焊接工艺评定标准 →", "content": "查询焊接工艺评定标准"}, + {"title": "查看防腐涂装技术规范 →", "content": "查看防腐涂装技术规范"} + ], + }, +} + + +def build_agent_json(agent_id: str) -> dict: + meta = AGENT_META[agent_id] + return { + "id": agent_id, + "name": meta["name"], + "description": meta["description"], + "prompt": meta["prompt"], + "background": "string", + "access_control": {}, + "default_query": meta["default_query"], + } + + +def create_agent(agent_id: str) -> bool: + resp = requests.post( + AGENT_API, + headers=HEADERS, + json=build_agent_json(agent_id), + verify=False, + timeout=TIMEOUT, + ) + print(resp.json()) + print(f"[CREATE] {agent_id} -> {resp.status_code}") + + return resp.status_code in (200, 201, 409) + + +def main(): + created_ids = [] + + for agent_id in AGENT_META.keys(): + if create_agent(agent_id): + created_ids.append(agent_id) + + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + for aid in created_ids: + f.write(aid + "\n") + + print(f"\n✅ 已写入 {len(created_ids)} 个 Agent UUID 到 {OUTPUT_FILE}") + + +if __name__ == "__main__": + main() + + + + +# import requests +# import urllib3 +# from uuid import uuid4 +# from typing import List, Tuple + +# # ========================= +# # 0. 关闭 HTTPS 警告(开发环境) +# # ========================= +# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# # ========================= +# # 1. 基础配置 +# # ========================= + +# BASE_URL = "https://devb.zdht.hjkl01.cn:65432" + +# AGENT_API = f"{BASE_URL}/api/v1/agents/" +# ROLE_BIND_API = f"{BASE_URL}/api/v1/agent-roles/association" + +# HEADERS = { +# "Accept": "application/json", +# "Content-Type": "application/json", +# } + +# TIMEOUT = 10 + +# # ========================= +# # 2. 角色 ID(数据库真实存在) +# # ========================= + +# AGENT_ROLE_1 = "2" # 助理员助手 (1-4) +# AGENT_ROLE_2 = "3" # 机电长助手 (5-6) +# AGENT_ROLE_3 = "4" # 维修员助手 (7-8) + +# ROLE_NAME_MAP = { +# AGENT_ROLE_1: "助理员助手", +# AGENT_ROLE_2: "机电长助手", +# AGENT_ROLE_3: "维修员助手", +# } + +# # ========================= +# # 3. Agent 元数据 +# # ========================= + +# AGENT_META = { +# str(uuid4()): { +# "icon_index": 1, +# "name": "修理工程管理", +# "description": "对舰船修理工程实施全过程管理,包括任务分解、进度控制、工序协调和资源调配。", +# "prompt": "您好!我是一名舰船修理工程管理专家。", +# }, +# str(uuid4()): { +# "icon_index": 2, +# "name": "器材统计与分析", +# "description": "对舰船维修保障过程中涉及的器材进行统计分析与趋势研判。", +# "prompt": "您好!我是一名舰船器材统计与分析专家。", +# }, +# str(uuid4()): { +# "icon_index": 3, +# "name": "修理法规知识查询", +# "description": "提供舰船修理相关法规、标准和制度的查询与解读。", +# "prompt": "您好!我是一名舰船修理法规专家。", +# }, +# str(uuid4()): { +# "icon_index": 4, +# "name": "船舰知识查询", +# "description": "提供舰船结构、系统组成与功能原理等知识。", +# "prompt": "您好!我是一名舰船专业知识顾问。", +# }, +# str(uuid4()): { +# "icon_index": 5, +# "name": "故障排查与修理", +# "description": "辅助进行舰船设备故障分析、原因定位与修理方案制定。", +# "prompt": "您好!我是一名舰船设备故障诊断专家。", +# }, +# str(uuid4()): { +# "icon_index": 6, +# "name": "舰船机电知识", +# "description": "提供舰船动力、电气、液压等机电系统相关知识。", +# "prompt": "您好!我是一名舰船机电系统专家。", +# }, +# str(uuid4()): { +# "icon_index": 7, +# "name": "现场维修作业向导", +# "description": "为一线维修人员提供现场操作流程与安全指导。", +# "prompt": "您好!我是一名现场维修作业向导。", +# }, +# str(uuid4()): { +# "icon_index": 8, +# "name": "舰船技术知识百科", +# "description": "综合性舰船技术知识库。", +# "prompt": "您好!我是一部舰船技术知识百科。", +# }, +# } + +# # ========================= +# # 4. 构建 Agent JSON +# # ========================= + +# def build_agent_json(agent_id: str) -> dict: +# meta = AGENT_META[agent_id] +# return { +# "id": agent_id, +# "name": meta["name"], +# "description": meta["description"], +# "prompt": meta["prompt"], +# "background": "string", +# "access_control": {}, +# } + +# # ========================= +# # 5. 创建 Agent +# # ========================= + +# def create_agent(agent_id: str) -> bool: +# resp = requests.post( +# AGENT_API, +# headers=HEADERS, +# json=build_agent_json(agent_id), +# verify=False, +# timeout=TIMEOUT, +# ) + +# print(f"[CREATE] Agent {agent_id} -> {resp.status_code}") + +# if resp.status_code in (200, 201, 409): +# return True + +# print(" ↳ 返回值:", resp.text) +# return False + +# # ========================= +# # 6. 绑定 Agent 到角色 +# # ========================= + +# def bind_agent_to_role(agent_id: str, role_id: str) -> bool: +# payload = [ +# { +# "agent_id": agent_id, +# "agent_role_id": role_id, +# "is_active": True, +# } +# ] + +# resp = requests.post( +# ROLE_BIND_API, +# headers=HEADERS, +# json=payload, +# verify=False, +# timeout=TIMEOUT, +# ) + +# print( +# f"[BIND] Agent {agent_id} -> Role {role_id} " +# f"({ROLE_NAME_MAP.get(role_id)}) => {resp.status_code}" +# ) + +# if resp.status_code in (200, 201): +# return True + +# print(" ↳ 返回值:", resp.text) +# return False + +# # ========================= +# # 7. 主流程(先创建 → 再统一绑定) +# # ========================= + +# def main(): +# pending_bindings: List[Tuple[str, str]] = [] + +# print("=== 开始创建 Agent ===") + +# for agent_id, meta in AGENT_META.items(): +# try: +# if not create_agent(agent_id): +# continue + +# idx = meta["icon_index"] + +# if 1 <= idx <= 4: +# role_id = AGENT_ROLE_1 +# elif 5 <= idx <= 6: +# role_id = AGENT_ROLE_2 +# elif 7 <= idx <= 8: +# role_id = AGENT_ROLE_3 +# else: +# continue + +# pending_bindings.append((agent_id, role_id)) + +# except Exception as e: +# print(f"[ERROR] Agent {agent_id} -> {e}") + +# print("\n=== 开始统一绑定角色 ===") + +# for agent_id, role_id in pending_bindings: +# bind_agent_to_role(agent_id, role_id) + +# print("\n=== 全部完成 ===") + +# # ========================= +# # 8. 入口 +# # ========================= + +# if __name__ == "__main__": +# main() diff --git a/agent_ids.txt b/agent_ids.txt new file mode 100644 index 0000000..c2e48f8 --- /dev/null +++ b/agent_ids.txt @@ -0,0 +1,8 @@ +17cb4f33-f84d-473a-9299-d8138e8ea9d6 +a8352abb-900d-468a-97b1-ac93f012ccb2 +f6363563-fd44-4c11-aa24-d2d97abbaa96 +2fc6a72e-69a9-497e-b7f4-12cd6833c781 +16c1320c-444a-4fb7-9e2e-34748f16d361 +c742915b-4bf1-47e3-b64f-903a4ce60de8 +6089a40f-a1b3-4514-a229-25bc8f42789e +9cc84831-747e-4cdf-b941-736dcdc9514c diff --git a/agent_role_request.py b/agent_role_request.py new file mode 100644 index 0000000..a70cc36 --- /dev/null +++ b/agent_role_request.py @@ -0,0 +1,156 @@ +# # bind_agents.py +# import requests +# import urllib3 + +# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# BASE_URL = "https://devb.zdht.hjkl01.cn:65432" +# ROLE_BIND_API = f"{BASE_URL}/api/v1/agent-roles/association" + +# HEADERS = { +# "Accept": "application/json", +# "Content-Type": "application/json", +# } + +# TIMEOUT = 10 +# INPUT_FILE = "agent_ids.txt" + +# # 角色 ID +# AGENT_ROLE_0 = "1" # 1-8 +# AGENT_ROLE_1 = "2" # 1-4 +# AGENT_ROLE_2 = "3" # 5-6 +# AGENT_ROLE_3 = "4" # 7-8 + + +# def calc_role_id_by_index(index: int) -> str: +# if 1 <= index <= 4: +# return AGENT_ROLE_1 +# elif 5 <= index <= 6: +# return AGENT_ROLE_2 +# elif 7 <= index <= 8: +# return AGENT_ROLE_3 +# else: +# raise ValueError("非法 icon_index") + + +# def bind_agent(agent_id: str, role_id: str): +# payload = [ +# { +# "agent_id": agent_id, +# "agent_role_id": role_id, +# "is_active": True, +# } +# ] + +# resp = requests.post( +# ROLE_BIND_API, +# headers=HEADERS, +# json=payload, +# verify=False, +# timeout=TIMEOUT, +# ) + +# print(f"[BIND] {agent_id} -> role {role_id} => {resp.status_code}") +# if resp.status_code not in (200, 201): +# print(" ↳", resp.text) + + +# def main(): +# with open(INPUT_FILE, "r", encoding="utf-8") as f: +# agent_ids = [line.strip() for line in f if line.strip()] + +# print(f"=== 读取到 {len(agent_ids)} 个 Agent ===") + +# for idx, agent_id in enumerate(agent_ids, start=1): +# role_id = calc_role_id_by_index(idx) +# bind_agent(agent_id, role_id) + +# print("\n✅ 绑定完成") + + +# if __name__ == "__main__": +# main() + + + + + + + +import requests +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BASE_URL = "http://192.168.0.46:22000" +ROLE_BIND_API = f"{BASE_URL}/api/v1/agent-roles/association" +HEADERS = { + "Accept": "application/json", + "Content-Type": "application/json", +} + +TIMEOUT = 10 +INPUT_FILE = "agent_ids.txt" + +# 角色 ID +AGENT_ROLE_0 = "1" # 所有 Agent 必绑 +AGENT_ROLE_1 = "2" # 1-4 +AGENT_ROLE_2 = "3" # 5-6 +AGENT_ROLE_3 = "4" # 7-8 + + +def calc_role_id_by_index(index: int) -> str: + if 1 <= index <= 4: + return AGENT_ROLE_1 + elif 5 <= index <= 6: + return AGENT_ROLE_2 + elif 7 <= index <= 8: + return AGENT_ROLE_3 + else: + raise ValueError("非法 index") + + +def bind_agent(agent_id: str, role_id: str): + payload = [ + { + "agent_id": agent_id, + "agent_role_id": role_id, + "is_active": True, + } + ] + + resp = requests.post( + ROLE_BIND_API, + headers=HEADERS, + json=payload, + verify=False, + timeout=TIMEOUT, + ) + + print(f"[BIND] {agent_id} -> role {role_id} => {resp.status_code}") + if resp.status_code not in (200, 201): + print(" ↳", resp.text) + + +def main(): + with open(INPUT_FILE, "r", encoding="utf-8") as f: + agent_ids = [line.strip() for line in f if line.strip()] + + print(f"=== 读取到 {len(agent_ids)} 个 Agent ===") + + for idx, agent_id in enumerate(agent_ids, start=1): + # 1️⃣ 所有 Agent 统一绑定角色 1 + bind_agent(agent_id, AGENT_ROLE_0) + + # 2️⃣ 再绑定分组角色 + group_role = calc_role_id_by_index(idx) + + # 防止重复绑定 + if group_role != AGENT_ROLE_0: + bind_agent(agent_id, group_role) + + print("\n✅ 绑定完成(角色 1 + 分组角色)") + + +if __name__ == "__main__": + main() diff --git a/app.py b/app.py new file mode 100644 index 0000000..102fe31 --- /dev/null +++ b/app.py @@ -0,0 +1,1127 @@ +"""app.py +FastAPI 主 Agent 接口 - 修复重复执行问题 +提供主脑 Agent 的 HTTP API 服务,支持流式输出 +集成 LangGraph Checkpointer 实现状态持久化 +""" +import logging +from time import sleep + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse, FileResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import Optional, Dict, Any, AsyncGenerator,List +import uvicorn +import json +import os +import uuid +from main_agent import create_main_agent +from utils.function_tracker import ( + register_event_callback, + create_stream_event_handler, + unregister_event_callback, + clear_current_stream_handler +) +import asyncio +import importlib + +from workflow_registry import WORKFLOW_CONFIG, VALID_ROUTE_FLAGS +from main_agent import extract_conversation_title +from workflows.history_manager import filter_image_urls, preprocess_from_request +from config import set_request_user_config +from checkpointer_config import ( + CheckpointerManager, + checkpointer_manager +) + +# 创建 FastAPI 应用 +app = FastAPI(max_request_size=1024 * 1024 * 10) + + +load_dotenv() + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# app.mount("/picture", StaticFiles(directory="/app/picture"), name="pictures") + + +@app.on_event("startup") +async def startup_event(): + """应用启动时初始化""" + await checkpointer_manager.setup() + # 初始化智能体使用统计表 + from tools.agent_usage_statistics import init_agent_usage_table + await init_agent_usage_table() + logger.info("应用启动完成,PostgreSQL Checkpointer 已配置") + + +# =============== PDF 文件目录配置 =============== +# 创建 PDF 文件目录(用于存储生成的 PDF 等文件) +PDF_DIR = os.path.join(os.path.dirname(__file__), "created_pdf") +# 创建 word 文件目录(用于存储生成的 word 等文件) +WORD_DIR = os.path.join(os.path.dirname(__file__), "created_word") +os.makedirs(PDF_DIR, exist_ok=True) + + + +# =============== 请求模型 =============== + + +class AgentRequest(BaseModel): + """Agent 请求模型""" + query: Optional[Any] = None # 用户文本输入 + file_text: Optional[str] = None # 文件文本输入 + image: Optional[str] = None # 图片路径 + audio: Optional[str] = None # 音频路径(支持 audio 或 asr) + + # 路由标志(可选,如果提供则跳过任务分类) + route_flag: Optional[str] = None # 路由标志 + history_message: Optional[Any] = None # 历史信息(支持字符串或列表格式) + + # 接收参数 + top_k: Optional[Any] = None # rag 前top_k + rag_prompt: Optional[Any] = None # rag 提示词模板(支持字符串、列表、字典等多种类型) + image_kb_id: Optional[str] = None # 图库 知识库id + image_file_id: Optional[str] = None # 图库 文件id + text_kb_id: Optional[str] = None # 检索 知识库id + text_file_id: Optional[str] = None # 检索 文件id + + # Socket.IO 格式所需字段 + chat_id: Optional[str] = None # 聊天ID + message_id: Optional[str] = None # 消息ID + + # 知识库配置 + x_user_id: Optional[str] = "1" # 用户ID + x_user_name: Optional[str] = "testuser" # 用户名称 + x_role: Optional[str] = "admin" # 角色 + + +class AgentResponse(BaseModel): + """Agent 响应模型""" + success: bool + message: str + data: Optional[Dict[str, Any]] = None + error: Optional[str] = None + +class FeedbackClassifyRequest(BaseModel): + """反馈分类请求模型""" + feedback_text: str + + + +# =============== 辅助函数 =============== +def process_rag_prompt(rag_prompt: Any) -> str: + """ + 处理 rag_prompt 参数,将各种类型转换为字符串 + + Args: + rag_prompt: 可以是字符串、列表、字典等任意类型 + + Returns: + 处理后的字符串 + """ + if rag_prompt is None: + return "" + + # 如果是字符串,直接返回 + if isinstance(rag_prompt, str): + return rag_prompt.strip() + + # 如果是列表,转换为多行字符串 + if isinstance(rag_prompt, list): + # 将列表中的每个元素转换为字符串,并用换行符连接 + return "\n".join(str(item) for item in rag_prompt if item) + + # 如果是字典,转换为 JSON 字符串 + if isinstance(rag_prompt, dict): + try: + return json.dumps(rag_prompt, ensure_ascii=False, indent=2) + except Exception as e: + logger.warning(f"rag_prompt 字典转换失败: {e}") + return str(rag_prompt) + + # 其他类型,直接转换为字符串 + return str(rag_prompt).strip() + + +def format_stream_event(event_type: str, + data: Dict[str, Any], + chat_id: str = "", + message_id: str = "", + type: str = "chat:completion", + ) -> str: + """格式化流式事件输出为 Socket.IO 格式""" + # 生成 message_id(如果没有提供) + if not message_id: + message_id = str(uuid.uuid4()) + + # 根据事件类型构建不同的数据结构 + if event_type == "stream_content": + # stream_content 事件使用 chat:completion 格式 + content = data.get("content", "") + event_data = { + "chat_id": chat_id, + "message_id": message_id, + "data": { + "type": "chat:completion", + "data": { + "content": content + } + } + } + elif event_type == "function_end": + # function_end 事件 + event_data = { + "chat_id": chat_id, + "message_id": message_id, + "data": { + "type": "chat:completion", + "data": { + "stepsBlueprint": data + } + } + } + elif event_type == "execution_complete": + # execution_complete 事件 + + rag_result = data.get("rag_result", {}) + graph_result = data.get("graph_result", []) + final_result = data.get("final_result", {}) + title = data.get("title", "") + + # 判断是否有有效的 RAG 结果(非空 dict) + has_rag = isinstance(rag_result, dict) and bool(rag_result) + + # 判断是否有有效的图谱结果(非空 list,且至少一个元素的 nodes 非空) + has_graph = ( + isinstance(graph_result, list) + and len(graph_result) > 0 + and any(isinstance(item, dict) and item.get("nodes") for item in graph_result) + ) + + + # 只有当 RAG 或图谱有有效内容时,才构建 sourceCitation + source_citation = None + if has_rag or has_graph: + source_citation = {} + if has_rag: + source_citation.update(rag_result) + if has_graph: + source_citation["xxxx.graph"] = graph_result + + # 构建 event_data(title 由调用方异步计算后传入) + if final_result: + content = final_result.get("content", "") + event_data = { + "chat_id": chat_id, + "message_id": message_id, + "data": { + "type": "chat:completion", + "data": { + "content": content, + }, + "actions": final_result.get("actions", ""), + "suggestedReplies": final_result.get("suggestedReplies", "") + } + } + else: + event_data = { + "chat_id": chat_id, + "message_id": message_id, + "data": { + "type": "chat:completion", + "data": { + "data": {}, + "done": True, + "title": title or "新对话", + } + } + } + + # 仅在有引用来源时添加 sourceCitation 字段 + if source_citation is not None: + event_data["data"]["data"]["sourceCitation"] = source_citation + + else: + # 其他事件 + event_data = { + "chat_id": chat_id, + "message_id": message_id, + "type": event_type, + "data": { + "type": event_type, + "data": "" + } + } + + # Socket.IO 格式:["chat-events", {...}] + socket_io_message = ["chat-events", event_data] + return f"{json.dumps(socket_io_message, ensure_ascii=False)}\n" + + + + +async def execute_workflow_chain( + route_flag: str, + route_params: Dict[str, Any], + chat_id: str = "", + message_id: str = "", + checkpointer=None +) -> Dict[str, Any]: + """ + 执行工作流 + + Args: + route_flag: 路由标志 + route_params: 路由参数 + chat_id: 聊天会话 ID + message_id: 消息 ID + checkpointer: LangGraph checkpointer 实例 + + Returns: + 工作流执行结果 + """ + + print("子工作流标志:", route_flag) + print("子工作流参数:", route_params) + + if not route_flag: + return {} + + extracted_text = route_params.get("extracted_text", "") + combined_query = route_params.get("combined_query", "") + history_message = route_params.get("history_message", "") + + workflow_info = WORKFLOW_CONFIG.get(route_flag) + + if not workflow_info: + logger.warning(f"未知的路由标志: {route_flag},使用默认的qa工作流") + workflow_info = WORKFLOW_CONFIG.get("qa", WORKFLOW_CONFIG.get(list(WORKFLOW_CONFIG.keys())[0])) + + try: + module = importlib.import_module(workflow_info["module"]) + workflow_func = getattr(module, workflow_info["function"]) + except (ImportError, AttributeError) as e: + print(e) + logger.error(f"无法导入工作流 {route_flag}: {e}") + return {} + + print("zigongggggggggggggggggggggggg") + + try: + import inspect + sig = inspect.signature(workflow_func) + params = list(sig.parameters.keys()) + + kwargs = { + "extracted_text": extracted_text, + "combined_query": combined_query, + "history_message": history_message, + "route_flag": route_flag, + } + + if "chat_id" in params: + kwargs["chat_id"] = chat_id + if "message_id" in params: + kwargs["message_id"] = message_id + if "checkpointer" in params: + kwargs["checkpointer"] = checkpointer + + result = await workflow_func(**kwargs) + except Exception as e: + print(e) + logger.error(f"工作流执行失败: {e}") + return {} + + return result or {} + + +async def stream_main_agent_execution( + raw_input: Dict[str, Any], + route_flag: str = "", + rag_prompt: str = "", + chat_id: str = "", + message_id: str = "" +) -> AsyncGenerator[str, None]: + """ + 流式执行主Agent并输出函数调用信息(使用装饰器事件) + + Args: + raw_input: 原始输入数据 + route_flag: 路由标志 + rag_prompt: RAG 提示词 + chat_id: 聊天会话 ID + message_id: 消息 ID + + Yields: + 格式化的流式事件字符串 + """ + if not chat_id: + chat_id = str(uuid.uuid4()) + if not message_id: + message_id = str(uuid.uuid4()) + + stream_handler, event_queue = create_stream_event_handler() + register_event_callback(stream_handler) + + try: + execution_done = asyncio.Event() + execution_error = None + final_result = None + checkpointer = None + + async def run_agent(): + nonlocal execution_error, final_result, checkpointer + route_flag_value: str = "" + try: + from checkpointer_config import checkpointer_manager + checkpointer = await checkpointer_manager.get_async_checkpointer() + + main_agent = create_main_agent() + + initial_route_flag: str = route_flag if (route_flag and route_flag in VALID_ROUTE_FLAGS) else "" + + initial_state = { + "raw_input": raw_input, + "input_type": "", + "has_query":False, + "has_image": False, + "has_audio": False, + "has_file_text": False, + "history_message":raw_input.get("history_message", ""), + "extracted_text": "", + "image_description": "", + "asr_text": "", + "file_text": raw_input.get("file_text", ""), + "combined_query": "", + "task_type": "", + "task_classification_result": None, + "workflow_result": None, + "final_response": "", + "error_message": "", + "route_flag": initial_route_flag, + "route_params": {} + } + + main_result = await main_agent.ainvoke(initial_state) + + route_flag_value = (main_result.get("route_flag")or main_result.get("task_type")or "") + + route_params = main_result.get("route_params") or {"combined_query": main_result.get("combined_query", "") + or main_result.get("extracted_text", ""),"extracted_text": main_result.get("extracted_text", "")} + + if "file_text" in raw_input: + route_params["file_text"] = raw_input["file_text"] + + if "history_message" not in route_params and "history_message" in raw_input: + route_params["history_message"] = filter_image_urls(raw_input["history_message"]) + + print("历史记录",route_params["history_message"]) + print("用户问题", route_params["combined_query"]) + print("ttttttt", route_params["extracted_text"]) + + sub_result = await execute_workflow_chain( + route_flag=route_flag_value, + route_params=route_params, + chat_id=chat_id, + message_id=message_id, + checkpointer=checkpointer + ) + + processed_content = "" + if sub_result.get("response"): + processed_content = sub_result.get("response", "") + + else: + logger.warning("子agent没有返回response") + processed_content = "" + + final_result = { + "content": processed_content, + "actions": sub_result.get("actions", []), + "result_tag": sub_result.get("result_tag", ""), + "suggestedReplies": sub_result.get("suggestedReplies", []) + } + except Exception as e: + execution_error = e + finally: + execution_done.set() + await event_queue.put({"type": "execution_complete"}) + + agent_task = asyncio.create_task(run_agent()) + + execution_complete_received = False + rag_result = [] + graph_result = [] + while True: + try: + event = await asyncio.wait_for(event_queue.get(), timeout=0.1) + + if event.get("type") == "execution_complete": + execution_complete_received = True + continue + + event_type = event.get("type") + title = event.get("title", "") + details = event.get("details", "") + result = event.get("result") + + if event_type == "function_execution": + logger.info(f"function_execution event - title: {title}, has_result: {result is not None}, result_type: {type(result)}") + if title == "知识库搜索工具" and result and isinstance(result, dict): + print("===============================", result) + rag_result_raw = result.get("sourceCitation", {}) + rag_result = {} + for doc_name, chunks in rag_result_raw.items(): + if isinstance(chunks, list): + rag_result[doc_name] = [ + {k: v for k, v in item.items() if k != "text"} + if isinstance(item, dict) else item + for item in chunks + ] + else: + rag_result[doc_name] = chunks + elif title == "图谱检索工具" and result and isinstance(result, dict): + print("===============================", result) + graph_result = result.get("xxxx.graph", []) + else: + yield format_stream_event("function_end", { + "title": title, + "details": details + }, chat_id=chat_id, message_id=message_id) + + elif event_type == "function_error": + yield format_stream_event("function_error", { + "title": title, + "error": details, + "details": details + }, chat_id=chat_id, message_id=message_id) + + elif event_type == "stream_content": + content = event.get("content", "") + + yield format_stream_event("stream_content", { + "content": content + }, chat_id=chat_id, message_id=message_id) + + + except asyncio.TimeoutError: + if execution_complete_received: + break + if execution_done.is_set() and event_queue.empty(): + break + continue + + await agent_task + + if execution_error: + yield format_stream_event("error", { + "error": str(execution_error) + }, chat_id=chat_id, message_id=message_id) + else: + title = await extract_conversation_title(final_result.get("content", "") if final_result else "") + yield format_stream_event("execution_complete", { + "final_result": final_result + }, chat_id=chat_id, message_id=message_id) + + yield format_stream_event("execution_complete", { + "rag_result": rag_result, + "graph_result": graph_result, + "title": title + }, chat_id=chat_id, message_id=message_id) + + except Exception as e: + yield format_stream_event("error", { + "error": str(e) + }, chat_id=chat_id, message_id=message_id) + finally: + unregister_event_callback(stream_handler) + clear_current_stream_handler() + +# =============== API 接口 =============== +@app.post("/api/agent/stream_process") +async def stream_process_request(request: AgentRequest): + """ + 流式处理 Agent 请求,实时输出节点执行信息 + 不保存历史,调用端会自己保存历史对话信息 + 每次请求时创建新的主Agent实例 + """ + + print(f"🔍 [调试] 接收到的原始请求: {request.model_dump_json(indent=2)}") + + print(f"[DEBUG] 收到请求 chat_id={request.chat_id!r}, message_id={request.message_id!r}, route_flag={request.route_flag!r}, query={str(request.query)[:100]}") + # 1. 构建当前请求的输入数据 + raw_input = {} + if request.query: + if isinstance(request.query, str): + raw_input["query"] = request.query + elif isinstance(request.query, list): + for item in request.query: + if item.get("type") == "text": + raw_input["query"] = item.get("text") + print(f"当前查询: {raw_input['query']}") + if request.image: + raw_input["image"] = request.image + if request.audio: + # audio 字段支持 audio 或 asr,统一使用 audio 键 + raw_input["audio"] = request.audio + # [新增] 提取 file_text + if request.file_text: + raw_input["file_text"] = request.file_text[:5000] + + # 历史消息预处理:解析、移除当前用户消息、序列化 + if request.history_message: + raw_input["history_message"] = preprocess_from_request(request.history_message, exclude_last=True) + + # 2. 验证至少有一个输入 + if not raw_input: + raise HTTPException( + status_code=400, + detail="至少需要提供一个输入:query、image 或 audio" + ) + + # 3. 获取路由标志(如果提供,将跳过任务分类步骤) + # 始终传递 route_flag,默认值为空字符串 + route_flag = request.route_flag or "" + print("打印agent对应的route_flag:") + print(route_flag) + rag_prompt = process_rag_prompt(request.rag_prompt) if request.rag_prompt else "" + chat_id = request.chat_id or "" + message_id = request.message_id or "" + + # 记录智能体使用情况 + if route_flag and chat_id: + from tools.agent_usage_statistics import record_agent_usage + asyncio.create_task(record_agent_usage( + chat_id=chat_id, + route_flag=route_flag, + message_id=message_id + )) + logger.info(f"记录智能体使用: chat_id={chat_id}, route_flag={route_flag}") + + + + # 4. 设置当前请求的用户配置和知识库参数(线程安全,支持多用户并发) + # 如果请求中提供了这些参数,则使用请求中的值;否则使用默认值 + set_request_user_config( + x_user_id=request.x_user_id, + x_user_name=request.x_user_name, + x_role=request.x_role, + text_kb_id=request.text_kb_id, + text_file_id=request.text_file_id + ) + + #5. 执行主Agent + async def event_generator(): + async for event in stream_main_agent_execution( + raw_input=raw_input, + route_flag=route_flag, + rag_prompt=rag_prompt, + chat_id=chat_id, + message_id=message_id + ): + yield event + + return StreamingResponse(event_generator(), media_type="text/event-stream") + + +@app.get("/api/download/{file_name}") +async def download_file(file_name: str): + """ + 文件下载接口 + + Args: + file_name: 文件名 + + Returns: + 文件下载响应 + """ + pdf_path = os.path.join(PDF_DIR, file_name) + word_path = os.path.join(WORD_DIR, file_name) + file_path = pdf_path if os.path.exists(pdf_path) else word_path + print(f"下载文件:{file_path}") + # 检查文件是否存在 + ext = os.path.splitext(file_name)[1].lower() + media_type = { + ".pdf": "application/pdf", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".doc": "application/msword", + }.get(ext, "application/octet-stream") + + # 返回文件 + return FileResponse( + path=file_path, + filename=file_name, + media_type=media_type + ) + + +@app.get("/api/health") +async def health_check(): + """健康检查接口""" + return { + "status": "healthy", + "message": "服务正常运行,每次请求时创建新的Agent实例" + } + + +@app.get("/api/v1/statistics/fault-type-statistics") +async def fault_type_statistics(ship_number: Optional[str] = None): + """ + 故障类型统计接口 + 统计生成报告里面的故障类型,按舷号分类并计算百分比 + + Args: + ship_number: 舷号过滤(可选,如果提供则只统计该舷号的报告) + + Returns: + 统计结果,包含 faultTypeData 字段 + """ + print("故障类型统计街路口") + from tools.fault_statistics import statistics_fault_types + """ + result = await statistics_fault_types( + ship_number_filter=ship_number + ) + + if not result.get("success", False): + raise HTTPException( + status_code=500, + detail=result.get("error", "统计失败") + ) + """ + result = { + "success": True, + "faultTypeData": [ + { "value": 35, "name": "作战系统", "itemStyle": { "color": "#00C2FF" } }, + { "value": 25, "name": "电力系统", "itemStyle": { "color": "#FF4500" } }, + { "value": 20, "name": "推进系统", "itemStyle": { "color": "#9400D3" } }, + { "value": 10, "name": "航空保障系统", "itemStyle": { "color": "#228B22" } }, + { "value": 5, "name": "船舶保障系统", "itemStyle": { "color": "#FF1493" } }, + { "value": 5, "name": "其他", "itemStyle": { "color": "#2C2C2C" } } + ] + } + + + return result + + +@app.get("/api/v1/statistics/fault-frequency-statistics") +async def fault_frequency_statistics( + top_n: Optional[int] = 6, + time_range: Optional[str] = "全部" +): + """ + 故障发生次数统计接口 + 统计生成报告里面的故障发生次数,按频率排序 + + Args: + top_n: 返回前 N 个高频故障(默认为 10) + time_range: 时间范围(可选,默认为"全部") + - "全部": 统计所有报告 + - "近七天": 统计最近7天的报告 + - "近一个月": 统计最近30天的报告 + - "近一年": 统计最近365天的报告 + + Returns: + 统计结果,包含 highFreqFaults 字段 + """ + print("故障发生次数接口") + from datetime import datetime, timedelta + + start_date = None + end_date = None + + result = { + "success": True, + "highFreqFaults": [ + { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 135 }, + { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 103 }, + { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 86 }, + { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 77 }, + { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 76 }, + { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 65 } + ] + } + + if time_range == "近七天": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + result = { + "success": True, + "highFreqFaults": [ + { "rank": 1, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 10 }, + { "rank": 2, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 6 }, + { "rank": 3, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 3 }, + { "rank": 4, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 3 }, + { "rank": 5, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 1 } + ] + } + + elif time_range == "近一个月": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + + result = { + "success": True, + "highFreqFaults": [ + { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 38 }, + { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 20 }, + { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 14 }, + { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 13 }, + { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 10 }, + { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 8 } + ] + } + + elif time_range == "近一年": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") + result = { + "success": True, + "highFreqFaults": [ + { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 68 }, + { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 74 }, + { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 73 }, + { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 63 }, + { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 56 }, + { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 46 } + ] + } + + + """ + if time_range == "近七天": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + elif time_range == "近一个月": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + elif time_range == "近一年": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") + + from tools.fault_statistics import statistics_fault_frequency + + result = await statistics_fault_frequency( + top_n=top_n, + start_date=start_date, + end_date=end_date + ) + + if not result.get("success", False): + raise HTTPException( + status_code=500, + detail=result.get("error", "统计失败") + ) + """ + return result + + +@app.get("/api/v1/statistics/spare-parts-statistics") +async def spare_parts_statistics( + ship_number: Optional[str] = None, + time_range: Optional[str] = "全部", + top_n: Optional[int] = 6 +): + """ + 备件消耗统计接口 + 统计生成报告里面的备件消耗情况 + + Args: + ship_number: 舷号过滤(可选) + time_range: 时间范围(可选,默认为"全部") + - "全部": 统计所有报告 + - "近七天": 统计最近7天的报告 + - "近一个月": 统计最近30天的报告 + - "近一年": 统计最近365天的报告 + top_n: 返回前 N 个备件(默认为 5) + + Returns: + 统计结果,包含 sparePartData 字段 + """ + print("备件消耗统计接口") + from datetime import datetime, timedelta + + start_date = None + end_date = None + + result = { + "success": True, + "sparePartData": { + "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"], + "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"], + "counter": [86, 76, 43, 42, 33] + } + } + + if time_range == "近七天": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + result = { + "success": True, + "sparePartData": { + "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端"], + "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统"], + "counter": [13, 9, 8, 5] + } + } + elif time_range == "近一个月": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + + result = { + "success": True, + "sparePartData": { + "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"], + "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"], + "counter": [32, 26, 20, 14, 10] + } + } + elif time_range == "近一年": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") + + result = { + "success": True, + "sparePartData": { + "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"], + "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"], + "counter": [64, 63, 23, 20, 19] + } + } + + + + + """ + if time_range == "近七天": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + elif time_range == "近一个月": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + elif time_range == "近一年": + end_date = datetime.now().strftime("%Y-%m-%d") + start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") + + from tools.fault_statistics import statistics_spare_parts + + result = await statistics_spare_parts( + ship_number_filter=ship_number, + start_date=start_date, + end_date=end_date, + top_n=top_n + ) + + if not result.get("success", False): + raise HTTPException( + status_code=500, + detail=result.get("error", "统计失败") + ) + """ + return result + + +@app.get("/api/v1/statistics/ship-numbers") +async def get_all_ship_numbers(): + """ + 获取所有舷号接口 + 从知识库树形结构中提取所有可用的舷号列表 + + Returns: + { + "success": true, + "data": { + "ship_numbers": ["101", "111", "122", ...], + "total": 10 + } + } + """ + import httpx + import re + from config import KB_TREE_CONFIG + + KB_TREE_URL = KB_TREE_CONFIG['url'] + KB_TREE_HEADERS = { + "accept": "application/json", + "x-user-id": "1", + "x-user-name": "testuser", + "x-role": "admin", + } + + try: + async with httpx.AsyncClient(timeout=30.0, verify=False) as client: + params = {"kb_id": 2} + response = await client.get(KB_TREE_URL, headers=KB_TREE_HEADERS, params=params) + + if response.status_code != 200: + raise HTTPException( + status_code=response.status_code, + detail=f"知识库API请求失败: {response.status_code}" + ) + + result = response.json() + + if isinstance(result, dict): + if "code" in result and result.get("code") != 200: + raise HTTPException( + status_code=500, + detail=f"知识库API返回错误: {result}" + ) + kb_data = result.get("data") or result.get("list") or {} + elif isinstance(result, list): + kb_data = result[0] if result else {} + else: + kb_data = {} + + children = kb_data.get("children", []) + + if not children: + return { + "success": True, + "data": { + "ship_numbers": [], + "total": 0 + } + } + + ship_numbers_set = set() + + for kb in children: + kb_name = kb.get("name") or "" + kb_numbers = re.findall(r"\d+", kb_name) + for num in kb_numbers: + ship_numbers_set.add(num) + + ship_numbers = sorted(list(ship_numbers_set), key=lambda x: int(x)) + + return { + "success": True, + "data": { + "ship_numbers": ship_numbers, + "total": len(ship_numbers) + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取舷号列表失败: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"获取舷号列表失败: {str(e)}" + ) + +@app.get("/api/v1/statistics/agent-usage-statistics") +async def agent_usage_statistics(): + """ + 智能体使用统计接口 + 统计智能体的调用次数,按类型分类 + + Returns: + { + "success": true, + "data": { + "total": "8,794", + "types": [ + { "name": "操作使用", "count": "2,966", "icon": "repair", "color": "#00C2FF" }, + { "name": "故障排查与修理", "count": "3,620", "icon": "detection", "color": "#FFB800" }, + { "name": "舰船百科", "count": "2,208", "icon": "doc", "color": "#9153FF" } + ], + "ratio": [ + { "name": "操作使用", "value": 34, "itemStyle": { "color": "#00C2FF" } }, + { "name": "故障排查与修理", "value": 41, "itemStyle": { "color": "#FFB800" } }, + { "name": "舰船百科", "value": 25, "itemStyle": { "color": "#9153FF" } } + ] + } + } + """ + print("智能体调用统计接口") + from tools.agent_usage_statistics import get_agent_usage_statistics + + result = await get_agent_usage_statistics() + + if not result.get("success", False): + raise HTTPException( + status_code=500, + detail="统计失败" + ) + + return result + + +@app.post("/api/v1/feedback/classify") +async def feedback_classify(request: FeedbackClassifyRequest): + """ + 反馈内容分类接口 + + 功能: + 1. 从反馈文本中提取设备名称 + 2. 根据设备名称反推系统名称 + 3. 对反馈内容进行分类 + + Args: + request: 包含 feedback_text 字段的请求体 + + Returns: + { + "success": true, + "system": "系统名称", + "category": "分类结果" + } + + 分类类别: + - 信息过时:反映文档或手册中的信息已经过时,不再适用 + - 步骤错误:反映操作步骤或流程存在错误 + - 步骤缺失:反映缺少必要的操作步骤或流程说明 + - 备件信息有误:反映备件型号、规格、数量等信息有误 + - 安全提示不足:反映缺少必要的安全警示或注意事项 + - 图示不清:反映图片、示意图不清晰或难以理解 + - 其他:不属于以上类别 + """ + from tools.fault_statistics import analyze_feedback_and_classify + + result = await analyze_feedback_and_classify(request.feedback_text) + + if not result.get("success", False): + raise HTTPException( + status_code=500, + detail=result.get("error", "分类失败") + ) + + return result + + + +@app.get("/") +async def root(): + """根路径""" + return { + "message": "Agent API 服务", + "docs": "/docs" + } + + +# =============== 主函数 =============== +if __name__ == "__main__": + uvicorn.run( + "app:app", + host="0.0.0.0", + port=9088 + ) + + + + diff --git a/audio/test.wav b/audio/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..02b336c2689f2e265e9bd5517613e79aac50334e GIT binary patch literal 13615182 zcmeF)3HVOc`!D{zQPF@TDv3xUL^5V9bH->;B%+j%kg1H32BI<)6-5~uBtwNH%2X&B zQ)WU$sEEDKYd?O@x8FJ6GhF|3UH^0a|JUQW)@Sd%_8RYX5AXY4>v`N>w^prMU#t%9 zs(xq92RnAlpFIeIFvt+}pXcUKh9FarJ*Z!|X|pRl)@@j;QQfAsnq_rgzx5LjmMLGN zT-g$(N|p@Tw(izC&`SDWngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ> zngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJ zra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1Q zDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+ z3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI z0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5 zK$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt% zkfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGG zq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nk zX$qt%kfuPI0%;1QDUhZ>ngVGGq$!Z5K$-$+3ZyBJra+nkX$qt%kfuPI0%;1QDezAU zg#Uc_$Nm2({~tyEU%QRtAV|{LfB)_C+5BY8p-=KR`Ip@P>-p?`>eavf^Y8jd@}gA# zXL^nOJ6ri|PiOo6=W|l#?DN_3|KGa)Yo7m^`v2=$oT1d2TL11oY2j@C|GKqwiT@iE z_~-g0_h$AWJv_h2IORhHqK7kN6H>Ks?{@Gctz-A*n0FbHd9Zv%FzeNNSBJ_7v#wL+iVm zPmw9C7G{CDHbF(78~GV<@A2p6TK|E(rq*KOXy|& sV?>^h3B{3Lhf*7XNaQiqB!{RbtCuLIKn0yLbHk zLRwGXzt4JgmAcT%?NjzFu?<(iBbl7~%*h^KOC#o|-Ks$Ky*%HM*k%6m<58LT+30q= z_ATNGQRVnO`|eAkKcR&E?A9K!i|?cR#4LKoo1$0Z1$M1L<3Bb_5nqq8TV`gG?_wu+En2{YFc zS!zX^I@{xWmHu2|4L`T0FO!uXspw%pT>UJ3JW W! zriltpp;KS+zCV3A25L=q{d#)7gFdV;+Frv>QU$(d^6MbUZ_ivtiZ9^5#xNAbWf$OH znEF1E)Q0e5=isT7MR*XOBzs7&@J^@Vv*9#yJ2ji|L92^~RffkO5xIEFb%9pD7B`=Y z*T%!ur{U T@7FumpP?QgHuIRxZ^ytQv zCcA ;ep-)XxG_%?^>*Dv|>3+USI K|PacXREkRwE~P zii#s%3EyU=E6(FnTo8Q`og0mp) 7trEn7{QjRE)5{lSX@@3uv&@Q133tIHbp}gwcyZx~)*5zevZE zw9Hs2_y@E-%5&RBx|44lBqt*FpnCER_}OUlC0%fkra3@=92QHt7S6gc=pmjn+*!^Q z@YFPsnlYY}Z=!BM`9;n=T%L+CzsVNA!xAqcBVVy59|v{9x$OU{AO{PSC0vA0*Rq~J z;G(&7!#tYe8~S0j`%SRa*X+PFWj 8P<#4L7_!>uIb08EhZ z`7a`8`-2DYZdGxO>v4Ey*K=5}qIfw+xR4$iM3X!PW%Xup+wjdB;?&yw *5q4y^bLN*0P&`` zQ=0J-=ZFg7&k}h14-|Zt7dKXoDX?*}yEzEe>*CUD(Yw5y7C5~vy<3lVtwbN+$|J5} zb!zh 3>$Ve+g94U`=ILEwOPez%4uPX!6Zrc%((??>cZa>MYprQsr@fllQ|z7E?LW#R-@wZJWbCI{ zf|Iypzg^ShB tV>rk<|lR3LR0Tfvfi6m@ #eJh0(rH_#eCt&eBLCx_#Ue#Y31C4I`uCMO;1TFq;4#+9sjRdi_O z(>$fMF4TH}!xGQGJS L0&7eoezn!&St%3<);%jyreZ~vtEvcJ~iDWe-h0+OiF4qUN^ z=KsZKG4wVQ|Lk!+Xm4SWUXouB`3PTrOaCOlT~?8$DGN0uze8{ =H21;&OE8U+8V|e zc$!{&g|?bSr+sDzZZZ@!l<&};M(LoPu2BC|bmzT1hzjs?LnZFgW=+Vx0X^1)^{!6` z-c29g0f#hzRa%)@J2?4Xo fLFsck&0S@m0#BK|z{qwX>7E E&!>U!LM-XGpRrH;d;uANDx`BVEWZEDCc~*H$w+`aW$GF_SD%#P!BjR$Ha`qc_3e zrBS~QJJ7^Vd#&IW{`oL_h(GhIkK6M(;*{|=5xey?!;f@Ka(=Fhb?xFr!FU?ub?e=m z4Qk8UcOs#ka8g}1ryM KX7> U$ zdT0tu_X$n2lmvaALeevE`Wwcz*k_e@)9J6T?MEDtyAN_GCF(F$Wb}JjWHyZRyvXV> z6#og2oD)6^1-{6_yoypI@kdwumhi$7xb_!3^0RZU^I5rxyx-|T&6K_R5|508_8t|d ze+(AuW2fn9-rFRY?mHg!xgyz>!moon!x2G?aHOdI0((j`?KOR6&nQ<|o9A|}2zs`# zdsr-dA*>qqvCCD$zVzR`&uhba{I@#F)D>aBPei^x{8xwP)?Q4#g@}CZ@DaOaEyF?K z@NjgvBAgih7`_$$9nKH0%&<4SJj37NwHd+;B{S>`!wieUb@tcxggG)4$Z&p!8!{XZ zx7c5MJ=`CD86FA`hZ!?u%&;rWp5bd{)`e5TMd2sfIvjo$t_|0O?UX7LF0d1@GVBnp zw;!<5zS|r-1RvXVn`%dHupPV};{DGU=XiSn6V0c;HxGL17j_AI8)@}$nCQYnHD=fo z=wkQ1sr~tS=6ksrpL9wq7i4>a=lM2t{=U8R-LS clzJw$6XB8Lsg!VTi$)ubl=9(NXG8oo}>fo^wd_;a@O-PB1EoG*%3k@g*gpTEH= z@3JO6@YXQi*gJUXTh=j$h)Z6WIQi|J*{tbE+O8|UZ=p^te18XQ)}O|Fk%oT>z8FhR zM&r~O@aYP1`47m^>wMDb;*Fz~ABKOO;G;n=W8)gAUr4O(lo<3ybn;(O55DG8c6{5C zhKAypUE?>SXX5eEgYlwBEHP^2zJ!zG`9-}mI$6HXj`jv|?hW>!r^2{z!?>%WIegO} z;LC`&n?)S>xIOTLBCfwfojalAFYH>+gqi2d`-Ct19xrx|DD6DB_)|H1?W5&`t#iRN z=Zh^CAct2P^?9Ch@NjdJ&8$X$AzOA~ywpDRpX_Qw65W|i>xve=L>SweS$%dSIi>al zDZUkTPe%hqmV2{h!<-r#<$j>uyCJC)`cX7NobnU)c88en4t1BYT&sA++faNjg!8*y zG$*_1#)(kfLNdIL<(Odaqmq4@%Fy2vc13Q61Y_sPl3y0 ezz zQpsd*bpo5wnEbb+g}3rwN3tG=#UM{OCtQXPoJ5F5;K4CY!YrZbvxgt@z?MiWVAUDy z&TH(*E0FJSJlc; qcIgZ|u?G-R(rR zgzk#j2TH!*)l@6@uvOKRDT{~8u@+_IRVO?3(5y1+S{@>-&0lEjM13vipo)uw=0#00 zHQ4!ec6|gJKVIx%smSzRHsL5eehObFzgoD<%3Px7)9ijX&!;2rqGCL|@;t<1Jp3xG zZB4VSs_(jbOnyx|`CZDJQ~S@!uQ4aTR+yZIKVRfG`94bWYba%%QfO(Wy+sQ;u!rqM zN?*2rHiRtJHM)9cdKW&p5r!>DvhS8R9@_s@8`F3MFWY(T%`0dKU6g?W>g%y7t|$hn z6jjgJV7r3*AnL;`T&%BCPJSOveGBwC{U?!VtK|DPosFpu++5bFnsTBq*Bj@>`n^aS zc~GtpDphcbqKVVu^~4}*#v`3D?}%D<~&AeJ84q;qyLYPpm(ku!uGM3hhRzH--%x z%DO(t5_j@FjHbRjkQ3$dLHKo` 2YEH2Q*MIdE+S)DS;Z1iX;IRad wc3Are@v8Wv#8bg?C}=1bS+&t zU-|;NUQ1qof&8=Mu`)qdzEn#wqnGjALeafLEa~a^19)K$N&EylnJ8j2p3gale= =a1r!CJ;;ysH`^$Zi-9W;om3m z^#jgLwp6Dhju?a=KNOMp%3i>H)N|G<{>Zmqp0?xK8=pQAR6)sVQfFuXChKdhGf@Xc zA2P59$#0W1U_I`o32rim@}#pZoY@E#D=jLL{6a}q_$;&jlHXz~OWKle(VU{=3)$5u z4tLdnA=(9loQQE=%6CGa2ukp#ufQL9@J2)Esb}i!X#oi8Pd;N}0qTi=G;@mLc6-Bh zjkE%f{8k))i`ZuY{$_sPbGeSEo-Y^*ql~lmBUzGuyv!F#)ze}o&L{-!@mvdKYeQ8{ z@MLYasWLm#(JH@0M~{J-J|}yAeJ+|WN;%f Eig2NUS!xhSyS-_fl6JwM5*Vz3Sn4Ky<5jHNCq _!&maX;YiU99_!IQ@J!_Q%w>xz6YNMOwZ_q_~*)$~EkGdA?_HR{wIT z80&lmivD3d*YKg*ScON~{6Vn63^KNyeWI NNg*EB!XB)g5OIpl@fBvu(6R^1I&W+PP1D zFYpWTh1psiM`quFgQxl215M S`En35uB6!4n-vPvD0W*i`xAxW}N);;ye~PDYXWlFU7x` z@a)RePG3l}bCHWs`WhXQUw&N01|+|l_#P@GrMA%KUx`tCNHWHdr2+KGv(6tqVNb6D z4Oqo}3tI0+)Z)d{Gwp)M=!-V=!pn5W6yDB9&g*X1`=2D>F9;x~J-0&8U=irNk!Yop zYNEN{*|GRpY-)q2?c%(@@x}JQ>OZjSKk&nT6RAEA%0IzY@8jzwr$w)E%H|Ii{%3nS z_5jt0p!|yA_2I2>>rK$^qtNbi;Y+aVi!ks|=hh~L<-!lb(hzdha5mifwkYkR;gO(I z_ 1r8u%)zfmmQY|aQlT}GpEN|h}YH*|Kexl6&1cVETXp)MzPDD*g<~gL?e6< z`WVgTPh+iG@z#>xTwiTXl3%x7Y9%(X9?7ps|ApGgZ_6gXDVyIZz1+OGOK8?h*uJ9j z%g|cI>6VK0P9gla$NKHFI{BRBE5+-pY6ZH}kZ;i|FWY(T!18ytB7T#I9=gw(j&v@2 zrk#QrDSmzx4r`AyYU1W%_|u+z5Ym3)roo};UY4s9JMaw5G=$atj77~5;)-x +4_ZCwe60Udsiib2WxHX&|+!0O-N``Op z&tBl0JuCh{UW9*qYA3Y29kjvW_@Hn2r1rW7FNHmW(P45LuvhSC_)IVWcRVDL-&9Pp zPxx6d)-LJuVxt4YF~JMYXN?K_Nxg%o?T Rg%_cR6$ewY!_Ze8I2-)8zUN;x!9cI-&wQe_B3%^n2 zg!y8-<#WX3SH{2cVe^My#M#2Nu^qqosM!1h_-C1Shgs(gVPWT^F2LD;I6;1j#oiph z$%6Kg9>EvASn=-UwkPTAtIiWF<=c?hQJzxXaBh4{_*HyIXvZd87WW7@#_xvT#wEkq zcJF_tsgmE3-UEMKz+#s%&x+37H^nVY@j(@K_AYp@Ih0u%3ci_S`rk-k+BRW9J0yPd zEc83@;feUXFc**dD#$n&WPCC78?ySRA&VTSm<>KYDoUL}e4{k%T$8L #w%#IE}3SP96{t(_c zt;HpxEeq+7kLaBh5dAJY($gXR@w|(@qT?~NpFQj(ZZ$}ppuZjYhQY)zpSI4kH jT& z#FxouhbgzX#!llNz@NDpKR1;gvj0~DRmyv+Y>n#LJ$%ld(0rct5?XpU>vBZYXE8l8 z1unOD2=BDRCna#+{WQ`rwx9!>aGmc|UI7tx_S|3EOp_(Qp94{{jY)hd`9 Egj=qk2N5||4#dd;Y ze(+W{->I1BUHiShr|P;lT4D$JLvgEq#?X^*+zE!h)8FJ9ugNbIR+sY9ndR8(`yq=S za{9XO18F>yiiceS#s3t&puOo%DZQz^-tgM%5YfDNMr3a|dNF<{S{gqW%@+TC38tG6 ze-n+t=OaV|{BD{#Kc-ASV|g~(3@?q3$7;i=_PD2b@z+r=yW72;-nzrQ>x<7lAe!(1 zRCbs0HMLpJx!7j$B=qPNHL`P61}-Xqu9t` -hN>kVO zwgCSAi{@P_9<(0<%ObMv8|=8afr#a!u;t6BGK5z)TEyS!r)Z8}6IcY7ZH5CEi}!pc zH~A)C2)ks_fAW8eWv1nl{{^KWEL4 j2uzEvEAAG?UUB;OZ& z8g1M1G~2+0EzPWpH63Zcp}P|e&G5#(*8D;1)z?}+!Mm&gJ=e5agOnajdS2#LCcnk^ zDk-ubOeUL}XY$LU<6zkb#0YNW!&fz@TcMH`Vs106#pist-ZaI-H2T2!3t0C@^xx&Y z(+1Y)OV;c|zT;$gVIUh)i7w8Uir0M#3(i81fITe9b|k-y+f`irV;X(7av!s#tEI2$ z?zj1J)A?&Nd2&zET5t>;co(hJ+2>vRlvAO<7oo(L==2F 2NYzE8Klz{OmYCUAe7RU|7deex z$MNEm|Ih0Vc&D+qRbifT_62U|-}l%4lW=q|-o|(m_y#;S3U-?+3fzZG=EjT3d47AL z__aKwlKk%)`PIzl`-F#Sg;})!02IBS_UdBBgRN3WE7uy09zm7+?2mP^J3C2w4ZiNc z C%If)6PZ`mcePs0& z5
zM&OWB+S+6-cjB$GzEOD#ExX)`U4yH)MT^Cj z=Hue2wB#Fd-&f-Yx@ZsN_C1U+kCt92*7YYem;6r1jbyu!=u7f{$Xth}SJUl}Lb79d zIlbw=_D0+sht{{skE8tSbi?a><>8Ryi>$#Ek-pc(Gh4}TCa(hzs=ih$u?zJ@S+0a6 zPtc^LY5d!)cy>DWfWG%atlvAMzrcRm!gvc#`3^l6i(pM5Bk$_*Q<(cLJaNAhp;h$K zga>oCw%VfE0-7poxZH1|Wa2sIgc|PA z5vg@~l#~u(XNK8{8|8Ziuj7NkDAf%L>S5IF@%aO|why^}3jcOuQ@eTdl)H8?MJ@7o zAK4# qtk&f1741U^Wuj?3$p8*=aQcUe?BQIzciRYM8WGeYzNs6OJ zMzhFeEsDU0SJAU2c_@WM=Z={}^1u3At^Q@SYhHM~6n$G;M7=9?J&>k$5>y<#Cj9%D zl}vWerb3>-@dvZOq*v3O6~pE5|EK(~<@~!(>FuFVb8p!9S^D%@x-vPdG?fnh+=_ln zldn|&L!RL%8n?gIewc>sKm&J&=kKOruY+?hqnG`HCro^|)2>ZJ-<%Lbza*^B4|`3S z+4^{k&!5yhpyh-7q7&X7gKJlc%`YI|iS-z(#W!K(DXd2F9q!)r_W+RsCqMaq`)Tx< zN*=QJlq38JdfvtNN#@!I4U%6=e+RdXLX!zTAGjOKZ+X$RE1$C*kG7arEATx^(>qOA zg<(p3=8VjI-$s3wH{2gJJ`gKUeh;}6Ie1ZoV>y~MK=sRMpaFROV|Zq~R>p~-4^Qo# z7gC}&dfaOUkHY8mtj_hk%V!|<)7CD= -UvGSH5RNl&VCAK zkD(`Cp!XhQ*ZR@OL)f6(=(6+04-Q9lgWsYL#GI!}{vW1jzZmvnlCh4AB>!Jt%-_9) zUj0d&Y@{B0LP-6MY6O`WrS<1&otIe18SL5&J&&T-URI86AwPN9v@JC6MDIG%o42tf zm0;AwU${noQ~kcAm5 =!_!CB93vHue4SN1PbC$C@;@!jG!ilYf7=yOuoFr>)GHQ@-!xh=GttTW6Z8iB=WB z6(R2VkvIOm-v{xlkucG0cxR@)n}saWH+<0#;lOwB&@x=G0A~&`o>pYTndoq=ozg|V zLA=U&tEI5c0x^_XWO6$GdrdDx`F*2E(0uh)z(aG$*IYjLB7W!_dU=?P*TWwbA)(~| zo4XP2N`3|Kd^y>1$M-bkYzX=@-)(w2UJ*P(4~~M;#u#xoQGDMuu~$(*q%O04jn~+) zo{;8j7+@$ncR6iTSll&1lhf=E&SUXc!p?rNjAdw`?CsXDm34R!zx43j84|6@PTeN8 zvZA-J+qK!e;o@C0aa;2H4hL8ezw?3i$#35-wBobruaBkqEZ8O%EU$P(3#;%TiENbG zYkG(@yo5gQ@yM6*>Q|Dwx1CJuNBVoCQ8x&%9nJ78c@f{CE#5@!F-~kQ5UJZq+BcG* z-=s6qsWmB|`Flujo3gpYql${--5JgZ#)Zz$it7Crp6&ze$MXFj_~-tV%1TC*y-Je3 ziXxFs6xn1&h>S{QRb&&{imX&dAu7r$I|&VvB9&1x@+Eri`}2R@&+j$8+4zXWjF< z&hb9q=XqV%2s!#sa7zD))qlkZE(< Q;Nbvj6;zF~)cXUobZb0tb8b0#jcRY!x8 ziB&;?#E76ao}o;lLvWi)(@OAdWj5kYT3lG4+@#0ys70y8Y8GV2PQWU^V%`40j2_}Q zj`O|0@N#STq B7f+?Ou%PKdk%T@J77q DLN+sSczO&zd`!hP9$ct-KqVj%%KyTMc2ny!_F< zGF7!;)m(6DKKS*1QE-e&vXY6syjDTFnRLD1qt&t%n? bcqV)?e8=D5aBDar+!HSI?N{Mxzn=?_gxA7c zQNrl(>!LDI6kZ>l3%j|_hf(Dy6_$*0g@vN>VV&q`*fzQ~dfc8(>`^7!7Ty 3^nTewWxy(N;_z}f;>wDS&f(EF*?c1 zbcznEGCd(9b1|A`ocvp!eWlN Z~zTe`$lCjPS|{oi`E#_JXx%%1ta_ zONY~>zUi63>hz~BOfU-PFipQ!(95^N(9e^CWh~z!+Vv5Oy9pNh1Zwz_KR7|JPC^`O z$;E6@!a9<)9@<>aLM~wy`q2KyZ2D~6{VB*fp1bfW{P3U5KsD%KI=eTEj~qaw;@o)^ z-tTT4SZ6xeo7XmXA-guiww_dOCB1LM91B>`b?ofhvZlil|BJ?&k!Uu3orGVVoS24N zy~zJpVgolr=jXB2tznJg?Aspkqd5tE_oevq5WDb&`}&ex@5fAS;zj73% _6&9{wz^{=5;JrJhoPXH#&l#h!u%Wi z&XjXlVE@ZT*_6U~Uu9u_!N%1N?h3{R&HYvJS6w|#OJg{yvncgNsO?#v3aZ8*aOF V=dhMKe6Hh;nrCJhn!@fVA7#Orm%wegz2e)F^ zt9qv9wrVP#tOM=)+XE|(28G>cVHL%hprr!p uaBukmXP *BPZ!Os?cUZFCV6x?OY~xmF>g`b(=GYGLGYeU(FThNgwl|1*O|!3Umn zzHH3v+UPywkax+}ydMl!J5?vRiA^qJE`S>9j#8rZb1M9Eh~+bh?QasN=f!_!aHTlw za}7KDFYo%V{@A5%tDiP)0sZy&48x5?eu2$z3Wb)@_uciqYyeO2MmmC=tF9xiQ9Q*) z|4dqpUWO1aVmyr1@LHdjZK!gvHM`y(Vr$E?cNC|z=hYwOaoY0Q16cXjXiB_VRAVTi zEQD7|6mt^hdWEfSD))5{E1#S9Ei4i#K&FdAJ }gG&Ej>zfwfF%>Iky*O!R& zHo^M)u=R`N (6osXVt-_;_6_4ba}2pOZ*-`DO- z;P?f4WiK5&2K#&ufBnSn?4z3-w0GQJM_{hyUdO^xWBLEFEX`P!XsGjcb@l;{n@Sd^ zk=)PSbym7q!2g?rTa8zLNbc^l;3$s$Aj$ufl)O*6-;+rk>`AR1#wPux7jpzfX+aUs zQe57tqWn`mGF8-F<&aw}0`%DXDD=xc}I9Z(!+8^WE22&$zlFzq`tzRq|@}iY#df z5)xay>X6$tt$k2Td71>AN{^pwVGAFHAmbIn5Ay3r;oOa0XY2bfu|K&SQwgHHhBsL( z8Z`Epcm6Bpb`xP#Es+-8nfNOzn7EC-uZI(@2Y0JZRu@XzV1avaMIA8$PdnFe2(E+3 z^%jzK3Evz=ZSheRaE-U)bu#hXc~p?aTyh;%Yc _0Sv$BhC`;)FE;IM3T_iwGb28aAJeXAkbjn_}kr%x;D+d4F* zqdm;5FVA+4#(b~N^k}M3a4c0M_#QuSDRop|EwfK|XRAm19}!{R?=N0&GAj@Gmor?^ z!!dRp%gemNDm=}jR437CxQ}amPt1irpZ;4-QC}YN{I|%#`U7Tm`3;hLLoLY;Em}(J zWYo?nC?SBxOW}taz+X);JH7SpQZ)~U;fLSh^G|)>PCa@K`xV34H?vP8W8te~=kCNN z{~W2xGe+gCDv#WHw4ia-b=4^v 9AgO1# MVL0u0SPG6C6uln~i7toLqc%p{ z++;qU17QR6=`?_g=6bzs_MA*IG$+lu6Ug+KIV5V3S{40d{LNu?ELWk&bME#wS(_>( zEuXr|JXrcrMdVMaRr1Mn+$XzoE=s8!dB77^(-QY-iFg*gS0Uec-q9~{Rhz}Vv;EB? zmv58Qm85#N^Zg)Waf-}FzRjkprwA4(3w}|J8GQ7tK3vIW|Ae<9#eOR-t66}Z-;OW% zQiRr=#qT7te~q_@X9ydHY57Qu{{w6PGkH468l*@?Jlo(`EYs&u({{dQ0lV3W-Kony zwz5YrxM;Y&Vhg*_=ZUiBZ}X&w*fFD1RVBpU^){9)Hyf6RH$5W;S}z(}#YWC&H($ke z40lIu* O-i*YPd zFA+(1Ji;Uz@djCK=Ls9Kv_071_I%lctXgHP#2q5eJL%2?Shoh+tZAYermQ!XYJwRx zmYP}P65Lglm2M*jo9MdRc+_L8{B~_ISmf4^72oU$zJ$aE(U+J*td@TEkOu!FmXAjQ zB|Xc{{8DL_x-mxOxpdqZBh=ru %zi3gzA}|JkTDad z|4c&m;u+pz1xC7NKX=w3oww?%JzDT=@tA@}+P|1*%H{q`vM2exPioiLeq|wZ|H`JN zkzQ+5K+=6!&iN#|r8r_0J6T D3{z4WKO 5H O|xFum-)^gvZ&0uC)CX$Yd64{I)u mu|(v#p|aIf!oGl2inr>+Pq*N+WEd3!nhJ_yj=Tg79n0!VY=+g zM4sXmR=lTcR1yQX<~!E 8<#h9_C*nf5$_17E>r)^)c-NYFWU^r}5BvQHP> z>sH$*_W2bKzszr3bIi}q^*&p?5g(T|Sf_qyk7qy0Gyco& zA z@z~w<#wcg{n5X*Dl~ep!B@y#8!9VJF0uq!rI84r}C!@se$$#;4d3?@6uHxFf9IEwl z`HQqy2#040UJ;dS6$@MwA>QB|Ih`Y;>b~`|86OyBomq5{O$Fdvo~X9_eAF|4 Z%t2QbW7U 1#YHHLiHC|LSGLrZ9)cu{IBW~|GXL~`5^f24eYsR22XW5rW!=trO?dSZ<`#ix? zK4=-l`#wH#D-ZN_RM&4ey0@9(kI^t&mMBB&F(~|USRl12ten~%R!MyyCQ}8qPCLJq z)aqH?eRJoT>~ER2n$1%*<1wmen+965h|lN3Inl9jJ0JK}bep?+!SQ3nYQw~63wY{p zqIczVR?51Ml#d@RHkxC!&j{XlhPtC|V!I7m_G>8mtoZL!zrSPOugTDIIQUuCq(3{+ zk4GO!2K({o&E;E+0>Mh$C9=C$Eb=a-@T;TbJ<{z@A~7{RLv%@Wd4q8hXG!JXEKo$M z|6!4Ki&iE$@)=oyF?`o5?|&ARo_F32GCHr~l~;-8iYHoQHrmR>yaZ*(Yuv>%i)|J8 zZiO}0hyuTsJB`=i+TyO`wG?K`{mfD$un-;*H_6!Vvi&K?^rvVs-qP%Mom^}|`HkD0 z_Y~_GsF%IXD2U&A(s< BA7vDgoJ6C*iljjgIaZaJd{%9C^+A5_h9Cpa zl>?Vpp2w;bT*2O4aFy*UV}4+dueyulaP>AAKAt0Vgm|J3kJi#vo?)xIvbL?*%UU?u z61+quuhr8tm2)A~2|o9-Z@dC(JRe_SUhTB(|29#_mvT{iaU$PisJ??*eipG`(`Wy} zmGOFuhh_HuCUuuor(Dr5=Vg>Gkepj&3bXKn|6&Ag6K@xjU5RHK&k5%g)*6|#Tu#@_ zCA%CM=~PS%cb|AJvr35!Mt&5tl~gTl?P&3M5qHv0W^Af8B3{Sz#p^;`Gau)1pY?uv z6q487-^@dtcfA1fU76pmlOE~s6tDlRHutz%h`qQ2YdXulnQg(G@xPGkYEb?WXIY|x zssN7`&r}$xRXK;<%PSL9+MPduUF?7Z8V9$%Cz6=q`~%2BPy4qc8CChi+>qN1_{x;% z 2=QVqd6)6vUegmBFL8<6BJHP zG(vV^aDQ@f&^oy{7?LcMT$C)4{4|+8xi(ofIX(GU^7&-l y_*UZVwK`OW@`A?7=mW?3gE7~e&vVU+xvTkq^BKgIWuaP%+Iek=d3;g&ZZ8j6G z9&X;<4naB0dL4|wBRGNLv^ZbT8%t1^-abuVUxW9Dz+&-=#lvZ9XT2QflMm6&D $(xRbk%rJShPR>=q)o+B=GGb}-@oNQb_hQ(eQ9v zNBm!NjN<^vZ!T>5tNjXKt#fFxtC)fdwt$pWB~?%9<=5Z}<6cQxJj;0jJ(mUZSrq%w z7~biW8Yco@6AjdkuNXr%QBB)4{`qx&dlCQgH8lR6I=sEALH^(cGU0dcP{ngU-o6oQ z*@-WU=bn33?9+=L^~dK9h4hDD!(QX};<@wV8JOO*_YSPhcYNa4EO$JUL`po6LtIlL zcs4!aJMXBN;Y*1!E{bq&Wh<-LDg^9s&`IPiGb@)@Kj@sODmzg;*vATIm1j&-arK|~ z<$`l+6|6nOs;&t#v51+1IpUegEMy;+vZlWW GZ_-=~cnF9ZH^Ql|9-abaJ#`xxQ$N)Fx?+BZ77#iIIt?q>} zH$rjad^3SI$5pJOeLvH==0LZTobMet_kC7k6x`bnoBTLkywCdzDqD)HXwL@cWyM}) zB}2LBY!!FVhQ;W^%00`%yZ{Bi3Q@*$V=t7UdxtIg)HnN_F&^6xS0cqLlg0RSE(se? z#$LtA&tM~r3Uuw7YG7KkHqXF_gYCV@GoOYXv#3qarai7grzaui+u+}}YIeF{p_|DJ z)-w{@JmoONgR+I?u+sJ5>xS@W2Ng|=pqejbS>A_0-}2oenV_Xw 4umk1>g)b`Tqg&}iOL%oMY%v68>8N%x z9&_CpqtsdKUtQhNgP7UIFhnkOYdLHWiH%FZdc|Ry4s@rM==mm?B3>CeAL-AFdAI`t zjOT8)MhFDc27B7c86LsGmZVeheVkPT6VC_ths@+5_~u3kvmpJ-;`h?@s}BEkr)$?2 zuaCo9yv)P(O80t;eSX4q9(VP55a2pnUpc&cT}4F>#J3CP+yg7cRa@)SRW5+|78$qw zvX&TVbjuvQGf{uG)|V~yWqT6Pf@N>XinsLI!KW@>o60uaZ%oLYyudYilfQYY|M6mH zqb96&Epims%Jd>#1030iZED7AEM=c|slr{wb8RJk2YmY@ImuvEv#jc~POHKDlf+)9 zDlN|B7J-fbf`s4Xv1Z%*8x?cEsPkLp+wtTl_GV9zs}5q3cy+sjzFkRjXOV^X{QuCm z@A-Z@xtQQAGsyB#(q6;sO(gpS8@WHKhF5qHzi^MJI9qC7bPZGccT`=Ka7!M#r}(&Q zYMM;njOaD2f_i9PYqnT_bd;AT|4$@ZAl06wt-@=SG4A&s{-=Vfmm2(2X_0U#yu-u1 zL3L63&De+E!se-+VUg7Ka98w6I6s;YE|13Z6d#4-qaE1Z17U!9T@a?i^ vi`a91x!&sw?RUTi>K zW1`E5tBZ)UZ;Up1T`yK&<(utBN&k&ExQH*<>$tsq&>>!EE#BZ$zV$c{v;)Ji7GHZb zs=!0vr`6(7Ne`=rv`SZWP`~do`-NVlZ`Y}aJEJ9!M|a3_-i5_E5*3$U?O=5DWGG@W zq_CcSI6x22>d!2qi0tz6muTe?S cF!&@%tjk8v5wbc^b3*NTw;TGHLlzJmfNql z)5wA}pdc?)6pvRx)L2gbqKz2vaT?K_pBchmjiOm*l7;YV%Dyxb%eBSnb>gEss6-n> zv-ZmprP0$o!Bn1TJnnaf$S=l+W79EWNRxiD-$wR-p=0AU?M~1v)i$`k>^zDY8iSlc z1s)}XDvdZ}m6aE&0>M6*UjNdCA9{u->JRIV#r|%Gy~E&lEbS5O-Dkl*-0We9_fP&T zvRw@31s7o8?8z75 N_==5>mik_ooCYEM@1g= EWfXQJmB8$3(o@tVI0 zj-TypQ*qfVVD3LXNycPr_xz;zV~l%z%JuFEzBaaGb ?cS^Nk4zC4*lF6Hc0?CVs0?9Lpipiwo^JC1Lxc eCR(|?~KWx5`~ic5*d=`Y!BhzAM>2q z%op^ZeG3KOLJ!;Fk4;d;Pik_msJ{6J*Lq1GXG~TKa@mR}y9Z^GBi++54E_k;kML|= zyssTp)jm^#B6|E5y#FntnJDo|V3iSZ%xAt|EoNK>xy-}P55vp$6<0nPyaNaI#&QfG zyB)~ztz_^n65fRz# {PeZ$;ngm=)t*b+c7vf$yP( z1&w?!ubQWBdWFs#@@vCX0IyVUyF(6ci`f5nvs%QnYkXyl#yfb8 J;1aXY>^$vtLwnM8(EIIC;H@X|eb5oJBK5N24K-I3{~To-dx4E}m;Vp6TEW>vD`2 z`jUV75{B88K6>OAh U$_8OpTt}I0Bat`W$k8hRwv4lj0V9Z{MB&TgO}N^p6dJ?II2F|Rnfi) z-=9f6VS5wq{3h6u>K}Xq%bdVdCfUSts>tdkw%R5H?MTuS?(|-Dj>T~L)m^uVY(Pu* z(2gv`k|eV#rrzfXzkzu+v9}+4UB>pxm*8JEsuw9lj^eQa|LE&X>EqxwvhSN c=;=k+C?1UW}ha(f>Zg1dHQQ8Pu-lfJS?JY0HxM} z93H}$#w!TqB{i9O_FLeIjQTf0mTu8U#T<1HyjlzHY%FFi4wb|?f$LM<`1WRMD0=%m zC`zQds>Em(72~}VnEZET4|1!v7)GkT!F?~`4OfcSze*K>2x>+}QujnRCZg~(Pu~X9 z|6-~`^sO S&Dwkqw!bY2A0RIQ=SO>0gZxj#S0wQV4KN25tuXlj&9OQGElA^OvnTHM zT^lUL3w-2sb#nVeayRjEx8uz2&_6A0BcaYI^mwi)?gRL79XWkhzmC#d)G+lfMti z;bI)-+ukoH!K 5@MDo=&XRfAN|fyCJr{ zqMO{VK1j{jsB~mo+tJma;=Im$Ge2!rPWL!T)c7(D=*&y^r5W!)(eWCT{oQ*Rl9<8q znS5uo2D_IZX1*Oam|b0Oe#rGsXd#d1&BodkcHQFYFm5%rKAxo@pF7P4HDA&)N8Hy} z^!ao0v`Qqhf-Wzo>&qd-xXx&>BYPToJ4oeAFU;xyQu;P@8Lz=S4@&tE29C$V?(_Zz zEM_)bshI}xBALAY3H26cOUl_lO-3gh8`DJ=D_-ln8gyC|;;rWD)$uI1u{VWn4MgmX z#dD2BD39p5iX^)POk4!>ah_}*^>-FV`yT!}DmRjwG!!KnccebWkSw&VjqZn3E6d+x zNlj4!w1%`zCuv>ebsqCuYjWH~HmfEH>l~GXQ>&Rt{9*5J^Li`Eebm04 OeiP*P4G!awIo8%i@7boBYi&8sWjF3+r~CRL`WXxHmAl#;RW#na z5}X&W@m1a(R+eF^!aC 13Y!{4{Kog>b= zQLgL*9LECsHy%oTO(tuIXL-(%Q`Jm;0(0-CvAboI4!Yt+&wU* d{g2GF{v)3>y!HU27|RjP@Iqqbgom{odMFV)3;wbkeSq0`sM z%S18E5V7;CqL{g2o*^tycVj?D@-)vHcih-@`^yzgW_3E_>6+-rF7|68CMoXN0{Z_J zp6FC`FXpQ=?QQCgD#-KYbM!fFdEB#R!Ge`GBkDa^z&b3&OKQ~SV{BGo^R_w15mxC8 zDT&vp`B5aj&0Q=dWv{Z1ag;p`6Yz@r9pXOTaBl s4Fh5p>rOmBI{%1C0PjnHA zH{J~U{s3JmZrdJ}Nlc8YBxXg$Y#9^7qHKxhB4g$vD@S@iocG-p-Ie&$aX0eJ=FLmg zalR^A>;aloN1mvLJM2hThT||E7i(9+EOx?hbQQhV5;gY~eJ+*dn5%ARDW2cxVaT8} zjNe)u+b5k{$|HWx1m$Fg+q21=RA!T`cFEp6k4G9Iv-37g|1leJh`%}`19Q-}k3aeY zs{fO1y2`d>m&dt2h%@Kn=HLf7Xp_CC!9?-QtJP$UTH B%}X@GyEoA$P)a DSTt z{*mzK2>kpMGf6%Nm9~fdpN5?~!2S nF(Fj(>a&FtUNFGKNF zFN+^0t9bvy`=dCiJ?iE+IzFx(?+&@Y3S}>aeESB2a8`qyr>C6$3$k0Sf@jqcyo_~v z0&`{MC}?C4i@Q@Bexdz-V$aXA!+-MwN44MQe0aQ8^qcH`cNGgw#8B ~QF0qYT=& zy#8le{|h#IEq^cqo*BV<57BS&8c&6AekFL3jBI%tE+MOHTqmN+;I*JL=hHuhwBEy* z_ZBKFTIs*x&b*MniL2M*SSntlaubX(56?DI%XiglEz}@%P%+XP7uiYfZL}k1yXtt~ zy^_cr4E2Q1%I9@SbO~yTzp8lhCf>J|h3TRPUmyYFRCCYZ+m`ZQt6YD%^DpuJ$I$do zp8gLKa@v+f+?Go#UosZzoF4zr^{=Xk_|H?H dY9rX)&%FQHhp6ylm?PX6q$lX7y7simw=Ult1`|q1*N&@2%39qX( zpH6b0hxEHb@x6oT@|2@V<22R$a}sreQFzBm-mmt$TOR7DCkWk9K{9@aJ17wxGZT3% znZIbkg`QvpF7`t@bBZtj1 G?KIi%BlXN= zPrn%I+u$rJK=uC?xOKZ8_*(y-@ctYoIIdUug8alY5)9H$kJ7_?^lHjHK0m4){N5b= z7u4W|sa 3Rk43cxL;VbSv5-6kd}D_=@_ z#1F$DovyrZBNnHfZ6Yi=2jBOq_mgF&--eJTuu`wUD8u;h9{l!DDBpa!to#PvKhC7@ z@cM~kzhukz^2XbFgYS8c|JY`k)hOfxzv3UKW9j>vU8R~j;7ndKnOWsJsP9LWAp7MW zj$!NfV_Md$g?o=zTfk4fkGX#b!knc_Vro e`4qpkgh9kq9!#3e9 zVc+mv+Do>EVXpAru$aHX;gPhw;fA!L;g+=0;nK9*!ne~hhMUr|hda}LP1}>UEbZsC z{b@(jZV3;h
#nGXO%a|ZD!)95|e_rjpBKZ&3#<1 z;b@{A=Af f6?KCn5aMaoqUeU%c@^5vz)`?EvXPx0eRQ1olCcQ2T^534*eeKhGm{PiVIe+l0l&pLX8I)o(sItxFC; g>qRa;JIzzPDL7xxukg)}y zX%x(I-+$)&k7Z0I%aOli=BPP*><6sc>O|k*+r(3ztf}^>uRXfS;XWH|Q++v;1%DyY zmM`uan2Xl2V_bit^GxA+2gswxZTbRl*qPU@=KsTZraHK&GRBA% $wA8men;5 zd+LksFc&ZTCq`x;o4?Bw&DR3sS>PG0?s(a(=hd6^<2i=%u(Mt3Z7s6G_j_cS)|06P ztgNwvTHqX?uueb4Yer9Fg{NQ$hC( Xy Fgeu}r;*Wcw zl05Z%ahh@qCus_1kLf#&m5w zN31x{*%rH_c=eAJj($yKF$MDe$l11O!C&2Z 9caQ!EXUksHkxxmg zd%sQ1d&}r~)sU~qUrbT!xsE4Z9=$2^vdWX}cjuQpSt#N?89gKdt0Yb@q%~#EN$V4O z$$Y&k)22fVABfdfXxUHn=pns$h$d~LUEk@EYnYA9aL7%z3~Xg?dRpDshh`X!R&drJ z7 `OeV zWg`o^k$u?gbw7LmlL+OQqfUA~&i3!)xAwRrBlykqlG$Dpo#!}@a2N(#=57YUd7UA; zWe|VN1|P>6$5p)_LRvq-hS#Y<$|X;pUHp)nb-aZ|y-EJO2%l5e_OO^GOE5Ut&rW}- z^7vzxZVFF0o}XHnsLglX=d})dUXxWT3jx=Fe%rEi@r+Zm-06DW@woF}Wk)k7GX|xS zWo&hWBFV- 4}^@)Jkh$!=9i=0;UbRXq72R)4K5%vw*eMm_zi zph9wYP(>|M#pFkxVp8yEa(eJ^@-0s{-g*V?gKWv$gDlCDi93@!WGLQFWKF(ozJ#HP zBO m~C5XWb!BGSok7wRBK GozXK_$<6J_3_+n}PSsx5 zxpymVi7 KYUrS;1 (Z0|8y+tpk@Te=q5!R}Zm+vN$XiGm@)3vAJlHR<2Jf^5K#Mg%REy4TdR_T%y zWqifr?qreo;xrfXh;h#N1dH&yxZw&boE_hPfhYJ?{lhPK!lfAP!I;mkY;RL`Kb}!< ztcYR~25>ezzW^g0&nY;{oHN7Noq_yaXZ+Lyd}2wAUNIi^M)M|~P3OLT#o--RvGfVn z?GwE7QnQmyvCWXb9d0yG+vq_xLAj&Gs*UO!5mZC{Qt9Ze@b2h!bxec8HQ_Vt@4)c= z@L5*%33EbJ3BM2*?10@5g=NCu!tCL(Fk5&b{4edh@O;_};i `_s3S(FG9Q6U-cT4Akd zWY|91Xg-UxVQ+c#fyOT_HXie!*)DeR1N(S_lfL=QoZkPFuf7Q?_#bBN8vZdOkDmcT zD6Kb|KrZ*lIo*Y^iszY+*AI%v?0&7!eu9#A!axT^h`;OWq*)=#W0b3tNV1KK`pa0= z19D`i&E}Pb{9HA2`<2u?5ZfN{><$Qglju0b7gSIYnh5sFqpV}uSF_G>WO0i0o`H`u z27~B|_=JVe%=VU3h4hdZU_kIy`bZo4>-{M>`2w5!CF~3@iW8m+&L#%&Bcp@wR6(5( zOZ G>@)228}Rn#M85zh5&VvMxZ*s= z6T8^|pJD2ol7)j-$s2-4l3}83^4CQDq_tmIvmD9Ga-!Byg|Rav?`69mWTPKpMY|iT z)JhF$dG@<`@IO}NYgXp>pg+sI#P6S3iDe_Zx=ZHw3sz~R{Ow!(z!-I4dw$zU}rH;47?lPn=;T1(C8aATb2t9P9*U%FNfb%Ue7!l+%wr|n_EUS-jy_&;4% z{6*Vfnd^FS*4#e#ixYZ?3)*1R+5~a_y@S6#{LMho*l5-+UfX50``zVU56Efn!L%I+ z?vwAW>713FtDJMy^6TBMcO@~3Uz@J|r$GNR`L`wV!ZQOEZNVyj@ #7qOvhEZj-x79S7A&(fMRxv z4YrH(;#%^*;F`;%{2V?wyK&udy~SCwj*)?N4zO-Z@G2X8{)_y73WL55f1dMAnxk6K zj20Lc^%k i@>Ew|%1Fz&W--v_~Mc?KqpyVef MpBlxyD+Q zwf|LE@u+WZ5nW^v_hiM
- zmQj#u8l@Q{bHUh^%<6(}a%`I1Ng;gxeWI1xuy_-h$SNWVV *XzZl_e&)4M9H>PeEkvxns zXipwS$T2L`(~HQ#QjyC?+J1$IX&I}zMohAbw8e9?U*jP&s@*F>lAeT3C$NOWNbako zc_wyYES9eWDZd*-kqH{jhgYl!AD0&kx5afV#Az-PQ;(LDo&-sZ!F!Iwu`U*mkKz40 zVHdiIwHw*)f?uoKTGFbXG;KIOevtUOt>2p9N9#IgU9ooAM1dd=7NQi?T}gaiissc! zbPn!FGzd-`hkV*d=HocZ9oAYol3Js>Xe*70*He22h900^VY)iR#rTz>5Jv-<>sq9< zj%O*3zr9gC-E}zcztj$XXI`OyR5;{-b~53VZuZ2v;EK$&ygbCy+y71wMwxUj(_9T$ zm|k@JIr}|^kLu&vuNybrOO;b=*X?SoP6b;T*ECk!DD07HFP>9t(ZO%6jeV5sF^*?S z;%V=@vjO*;|Gv6oyQ(H^i{Ysw0~61aJJ>de9uL!l!|3A}o^B0f^AWv$g--XUSufDe zr)=ffhf9(9CZgX+=_&qlCpm *u(){vj~mb4 z|2-R)QVDrQt|*PgyOsB?%OcL@Yk$X*989mR$SfYZY~_}0xOKM3xDWL^Sm`Wy`ahw` zGwN<{BoCPrnSyJ^qyLk-Gsv1K9o&tpFOU1_knV*u1+B L2KaBj6<1-&kOSZ3;Tv-WodektdM;kW$88SB9 zQ)A+Ad$sU<-Q>tdL%O5f-yC@3Z4u;P-%OUv+2M|syX$yW_a%w{-1#{;cprZMOGhnq zck$?&@s6G5DcjT3+Bp12WjUINChLk5YeLu$`mVKhZK18(K;L7uM_(FH%n_v=SI*TV ztIcQ7CbzoM2|BeiRWCT0>JogNDr?@5JG6Nob 7;B)6>^iHBM9JJnDWP0eEM<8^gEQt$YtdizBxGT-z5Dc{tB6(``K55V9X zF!}GxbjPcRt+mI;xP-07TrPmdM;oa=-gjO7QrS74gwogJ91hv*2mJpE $^YRV3c&F05Lshu8mS?P|#$SflbN=o%!R}v(de~<|>Ni^ZZS;&R z^Fm{2x2QQW%S^Q1@0V%%?Q*$TTv(x;9%vySJb=y(XUFESG#kvVbPx);DqmYzPuCy` z{m9NV{N+TkmXTp{qcMy6izw` 49D9vDzUbZ)tb*B+<(FSHhsk8 &+XTOj!QU)o!QZv5 za*c1Htbf#(WD$?%3Qn2D;%q8I@Js3j75j AT1D>L7nT?DYT(R?9EVF-niP(`L@u zLQg!wHViORKsQfO+xhD7@bTK~IrUrwA?ByeMWE5!%{F|WQK|Q`Y5(EbPLrzZoHdQE zpEc?;@;R?8ab+mre!0H8S?nUX>Y~Qy=QJ+yGT-p6Y~E&`%UT~aKCZ<(4%_~W(fkpn z+%8I60s+3^yXSegUOdZ-qV;BSe>d@S`He7W#M@P1Deh)ZTHtZ(^DvohQ53I^^sai1 zwY=Oq=w~Hv`3u>n1K8~Ybn22>IWi@VL>JkSGb%S!Ax9_a%s&uQG7;;}j_9g!OM#k* zpHxDBq^{?EK5rV_^@`|n0`%0MWvb3fm4&Nv`2JwjLbU%B2H+`Ye~=cI<00EPst0DE zB`ekr3Tq#8lIl;UL4{j<-jrS`U7KBO&NDs%VYl=A@rsX4eAm+FmRh$GRQ!OaXe%F9 z!kLopIbN@!f!9hf;gkNa^UXmxFkUOt9MCXzH LA^-oq`i(CW573=Pj;|Pe0&9NCuP+vpccdc))koYe$5s_?z0(t% zHpcagyyESwVP?8@-c#oCTV=WuuLhJEt|&yWRJ%gI>(t5ppbqwbEK|0is%Os6E8l>Z zEzUA#r$fignR6wz!MT2rr#z^24~nL?s$v_TZe1c6FP>31Gp!D2aT;8BJ?!v@N}6N1 z>kH5kNq`s%!m@=_!#x0_# }j-fP*T9CE+ zhyBk_4|~BQlj!vVoY_jfwv^5OKrZlWPrjR-SdC5l$m>#QcoM9zoNYKmhjYT-cgYZz z4Mwv|-|5%w+I_5gjMHjl%8Qqm$I`)G#A_RTz|OqP3p_z8`f251?EhpE6|ZJ61)FES zOn%}SN34KjVzwl5)e~&nO1(1{itFo}!MMV=NW*NtXT0881QEW&!YzVS4r|R@f^)V@ z`Z7f(ON-a52k()9`K0u79LRRPzE_>hF$ivl?Ol7VV}aK>{vA=rMrg-4W=wod`^00y zUe||{Jn!9PF|Qi%iSA|re&@71GV&PqI^_6c&hR;Z&{MlLf~xvqCC8GE@oceNrY9O> zj~6}72vXjkuPpDARjNb|bI3*~GS&?ydV(jb&O)}<@00Xxce0UJw(nMBbSkTnZeZTx zCNwrL&HX|aC|(6&iCo`w{N-radYrx<1|juG&ueuTAJ>7Onz9isS;YEqRSl6q6H?L9 zp5?@H=cCao!#bo_q^^fH&cZVx>~aR)$(pJQUsR6rn$4$TsuOG05s&$*tkCze4hFKlRaviV(KZprMtQYOtl=8=^)0c( zP#LS%?A875y@Xn_60Bim$Hlg^v52=f`~IrPer9SyG*{*0d^NJ0cwfovgzRp4 z)x#BV5Ng)g>w~<;1^%R%HO5+~9PLbBx@*r-^k@Q9{ aVF zwDnJOgPow`%UJbcIF@m&|Eui!Q(E|CG1^QmHb=bwp*Vk$zIaKS4)ys(_%L2m;V~J( z?tDQ@m4)4LqVv3t(GP9>wuKKni(9zfaZB(XbG570(2d}!%kvc{Ip#0XGx@O(ZTZ$W zc)gw?yQkd8T=F*qUVlDa0)OFmzvX8S`sNqXxle|9ji~cO{r(A=HO~NcC}!9v!256G zb2fOAx#V+*IIx%fA0;WxytWZN#%u00bN&bYT7rMi#)HQ5 z!Y>dTtn~gp$Gn7>Ux1gtAbL3K^|E-NT x14i@3z(%c zTD_Ee&cwSH^1P+tnNp(QPM&BMMr)R*9_xuF!z}UG%5_?DxjyWhUOzlX#`Xzq{IIru zMr(}GlkqAT-)Qwr*ot_SyS%EGYhnW0s$TA5{=TvD$8VGNQ5c06MO_17`Is@B!8@$R z+r3U|Mv$b@WN!p1dX<-o$0)wftL=j9Hj`Czy|I5E@mO!brlZ7;)?Q%2PSJ&(vL}ma z%M6xlrqK>_>B~E8-3oHMT=l|yK7JUTQf}mlTs{w&f }X^_PfXJ`o@+N;G8Id;0Z;gw7-q9G zSy58_l1)v#6^iUzizmDjt|^2iX{ddk;whh}p(AMMYxM4QjLBsBIMudDj6It^PK0Dv zILB@{bdPg< L^8(7|bYA|+Nm2i``{*A^bSo*_e@Hk-0BkFn3)7RlIA7s^Y z@^PVS$KyTkW;gEh`*bycoBX~`+x?}@v++n*Sdsj}3>ajR4BAAQfqw8$clq8Iu*kht z-*l3F?TQ _hmj9$s6y_YN>$XUsq!cwwyP ze2x7Y$5VX_*?)l17y|{3X6dX9s147mQOP74D1(JAp~VVe$SUZaoMzuzs`u)X@%yp8 zP3gnq^q`w@nU9eCJT$*B*^h0?qrQ89O{oaK-kpx;t3b2`REm{kiEfZL` rdF$D^WxE{y{o+CoQZE!#{@IG#-Qg6>#pHY~bxA>^c_Y6o&O$G@h)?!hWrm z6WZ;1pVQI3EX*-`>}26SqkAi`X=X=aO9!%r^XTax-p5rnKhocLRAO#8FE5tnLC<@i zR;VEA>_bAPifu>Is_{Je8gg)qHkDTEIW>64=r1eJrDt+BCT8G2d#Bfren XfJDXKMdPb_Fzq-wSFB0xAqFosFpej3tzS0?Xug2 (J YmezTM>2j(y%J?ry la&(hw_5-;1o$6T*@(EvK zLT0Hyw8B&LD%(HV{yn{a7>X&tmYc1Shj=GCg{%1o^7$Qy^Dl;|4YXmUo9F@gfNb_J zqIN%v9Kr>@<$CA1Q(mDaKTwf$78T&JsRny}tPugKcTZjXz@wxH*QN~-4o*rV@ z7OY%-&s0l=d@&XBh2;^8YL^?d!*y7StC1)qTCRF(n%RzfCQe0V6SuYvxiFPE zSc8|_Oq0JeWBd{P+{f_B2k_+vvvkj8>BiaKP3OK#;-m|ztGx?5nV+@3Q^ej;9=v;c z4(c0N=MZnRgjab%7GM&5I+(SLS5&L#Ih*Rcvh<}Kq*|KhXVnh**@K1{zJ_pHIp{84 ztFH!o5YOM6!^noJP~%-R@4s|5Ylf)%6@I(1O0KeeZWZtI!L0Gzk-zZpXJmkn8By_D z6wiKAT)dl)O}U@mH%!lq V%vD`HPFl!h~YCCo7GUmHaOwyXbxs@f_JTX z?`lS{)aTc$Lb}C`ktrYLPTd`4mJ2GCx+`j6=CbN?L%EH&xyksMEJl7s#>ai6EI#-7wxliY*Nd08t>5veVOU5x=@ z4bMZ(m*IlK{8c%~|28ptF7sSxw^rRh&h#s;MCOGJZSAP1*` 0HT5zQ8 zRr4o|=ll3A{vu_RXg+=|9(DaZpZ5xMFc5dtOEmMm=y(u(Fc|kd3Mbf)x9JH@zadI~ zOO|{H?=?U?_6o~3kp=CiFFYF#<%a#pI%jP)wO8&+aHeox&REKBWSVZV|o`Ase4 zYB*;aN&LVV!Vg5!pU9=2;+Kx{K`VV;g(Li&Z#p0+b3^c5Du1xY?6cpfH93N*{$FY@ zE4|tI+Tx?`;Iq46!s7bZN+P#BJX%KFWerGwD8}q@(ONB^pWz1w!Q%t@#tE?bRH)v1 zyrQ?;M8-K;y9}bvD==8D^bAGCpaia?pqRRv`0zfSyM*ZP3EwTY-!w669Q*E3%li#H zunLm;)D;%-8UwZ3K #q8;rq5O7Vd({!^_VAozMY@mT4=bs!>FD>z)y5Qp*Gls; z9pJb6F~?%Bu3Ed7)_w#g>FB)wR~<9OGwy}yE{MZpt~LeP{RjR1=&nzSY4_Mx!V4M2 zzeO>O%OQv_#JIhE?id_`6fXMi8aDEnw*3PRxPTWv;hd-3{ofG9QG4(5T<`mBJghOo zxnC3=$0HFsYKwB(s}e-?7z|k#>rl#8-2IjI>;L6TdU)T;Uqk!FQBWzkqNT_!o?|?o zb3 ^8*z%N6bbA-KO-f1S}HQ6%{^4sgao1*Rdh_eP@J_hp0Ek$?j z^?f||u(@CPs{7qnFC$$>yZ6SP@g~Rrr^4e`nU3|= =ZnrM$^`LxX{94U-K>qlWVReny)H?FQ=Nh80&YB{9s+j zR%Qzmtkz%A=X}Tl7IdQD ?TuY;K}Mb>GsEGMaeg1HH~Q(*Ex4FdEO0OK zSV4?)g3rB${v}D*=X&=C67vVf^aIxNeZ9CA7M>>}SqvS&!^%#<#x7*v tZ}W68-_ax_6z$#6Wvvm|<; z-$(lG&2)J_@3%j+#; 3)FSz~y*d$s-DmHld^LDQc+%9 zNCvMrLL&K%tt=(pDB}4tlD{kXfm^hC({!0^Li(HNw-&Bb-*rmUj>7!?9j;OrtI?8F zH}niG^l$vy+ bXRlAhY`13h;k{pbymFS7p_6s;H)`v|8uWJ3$vQL3`f~ zR7o{Nwbn5EjZrT)%+}r5#?Gp(`kL*eS8#o@y-Kn6X4UAVTC9sovc4+Fo>Gh3M=kB^ z>R{h64@qw|y&cnMLfIzXm@hA|A~>JeAxHK>Fw30W6B5hSikNjnWmbQAf#(w+1>;m- zjZAC|;`-ZH)YtY+oEJy`8B9*B4c@iiw8Vx$-aIgB!#K`vqNAss^I0*=leUL~KgH80 zjqUrzGsnztJkoAEuKSEA=z0;)S(U8csQlW-`hUVJE>xYj%4n9~eD^0*vr8QJv27{O z@H!5Cl-x(WM&dBpLvwD5sh{T?I^fw`^V99{0`>W*$K^m`Z}oz#*$8&|d0swd6$Zm1 zZFsKRc&nRvflRzvf_E&AiK)m-<-zzAlEu9ZYCA;F_mI>OvdbWwbDHk|tQzAdI(wS_ z-v~YCW?Sz}+%AU7FIK1^^HbMWf$hH22#NYEZaH#%COT=1z#-`Nm}<(K#5J{TFS9RS z8cneux-}P5^cfqpjvfzX35KxCqi9A`wNH18%5y>1e?&9j^7r85b*io25LeA~*2Utf zNzo(bPJTKyB6`J)2@6w=qS2{_Q4hHLsno!zj`*!2r2eSdtdb)3e?(|GSfL!Q5zpz6 zk%ryHvX_-}Ys`|hr?K(cZtt1>W4$Qjb9Z%_#%Gfu{=?I4mp|OZ^6r9vk9c>Hy||fe zzKzzD7RwBgiCiY$S&Tu3c2Yk(ZnwP44?ONg9`YO K^;g67g7+`O&ClSqUKFWAh(7Pb zcU^=}GLyF~tVCI|Sr=kzr9HYk=i{(Ncbs5n_nQsgJ7fK_E2+Qn-A81)wm~mj b&Em&gPZ@=TTCPjCyr?FY!&vwDr ziD|}-O~Gx -a2KOUsSG@Y}ZFBNg{E69 sDHKc f!_e%7jR$s#SXXGiemE0i=#?AS6kv^by{NvMOrD@GcA zHyg`aVzsTR*{*YMKk3(lINt2qG&>2(=m|=AcMEp%q&fGFL0<=1i_P?58GV@qF^qyY z#?z4*G-y6enkf1lN2eCx?czCm=Htua(X$QUlNR=U%Q109*<)hK&dydB^Iyc)!1)G1 zfL+9%4Mnn@pqp_p$4Hp3JquCCo_E46Mk__d{=fG;hF`xd2X(W|d4bdhS%^19-^=lP zDsM0`CFFj}n6ITqY83Y7m8hyJ&4Q{i3#RU~e;1K^4^eoRD4riY4-8TaOH b+o}$KajDW@Bi^+BAZP+M45HC<``LW^5$(Xf!PJ zdTMvH*u8xvn}13?|2ux+A`6>6(S~JxRDQLIQK%1LK&xY88q%GPV&uoY)`SLY>Ytk8 z)>7ubu8KivW9^s@eE4hh@wE4aX>KW6-d s z7`<2%-X&qLJ0|8Sd$L iu48@J~CSD %I@t{3Oo-cXVByvTj9@A^S5wMFh#{nmn4yhD_|Mcz+khZv@e{NKH}`k0A* z(B}u;L0P&~lg+E>>bHoI%BmlV*)lb$JirXEZ{mPIfX-)OtEQ6wg^*)koOTOx`-+TY zKP*>cTU^&TiEPj2Q`Xa;+2W(8$@pO3J|nj1K|4m!iW$zd4yOCsIS-SOZ0;bAva->@ z>)lIHR=a{$slh@vVn^c{9jj=shuq^}|A(=DkBk1hvN%t9- n^b-YS^Gj~~Co7ZB^JBfO`usHQ0j~e>$F&?0$zxwW|mAieEWsf7t8X}-3 z`uSnc^|&awn{QjYla9_C-`}gwH`_f<;8&k@jnTNrHzC)tc=V@4!0q(L!+6HqFkMOY zte1KB9ioMmqK;4Di+A7+vyggyJ9;lQL>73wD5WpV&^j8-I`n5PDjTzSFFvjb%`ZXQ z?~yfrM9#1pi&R5g*i+=tgJpW0HEP1z)zPz!#WIiSwRV0T0#A$)CythJTg*0W5@YO1 zS%X5naaJ^v<{baYQXJLSKSJeaNXveE#xuB{U?s2We;H;LVWRkTlH=kz3f8fj@eGyw zNYY2n{Xu#p63@mQuY7lyE&A4dB}i8lv1Y~e*|A39TUS8)>tMMz#VNC0e=*kYU6Q$i zbjPvli}J)1^jDnU-o QNfckTP?(~ z)!?5(IOC%9E<3;bk0-m{cbRE!M3W23=TsmYCH?j&?N$AXfyl%EbtA>Su=>yPNz2Ld zmt^~6=h&s^{~#ON$-qn!F%@bWL&~4@zM*3uB7eOc^&S-SG1>lvhdjWpd=F9WaKvZW z#MR_#1KC{ZwF7y#wy>);a5q)RV*|2S4-51l4X*`h+(V`+iiDc50xjr5ykhDywqzcC z`H-f4>-b%)s eLJV29n&Kwk|lT>Tq`k2>raayZ{mXtF1D@0`c7X zchJ+@$Z}5ioRf@S(Iz`sfh96V%k=R!o^rV;bEqhJItg2 +ytTVfg&2OUpsem3_{SC%+P&ihc`k zOkK2P !asxC`^${iwly=l_9?lKp+>I7^{lJ? zdfe~VRJ{IA-sE~SrsYXJ?fA|xf8*%ZR6Y7v)47{O^-?cJ)nNU6=AkO>wHELFfN{nZ zQiHMOJ 5;jPE4a?{Yxrk<1njSDjA2o 744xtNl7Y8$`T@E%N9jM_B~4x35jf>kTtSIL|Ia#tRYKCM4_Y-UrEI? zbAIo6{{PqWnrF_LGs`_Q_kCU0=W{K$>%U4~4I!@vxWjiy!O_-(a0W}T5d1BAQYGu| zd;~+Gy7qci!oXE=uXkG13&)zQu+J84Kc=-OSYWxaPme)4t)ZJ1dT$5oH;wKgZ1!fP zeiItC2Z`Mkc4(R{weuPmH_$Rw*s@jFwf7sVkF)6;z$s4}uN~RuW7x aHqd1xAbg+^Cx=}Nj*69m5^mF7GXBfv~eZ~0emF+heP9D5xbPo5uv$2(UO@}&v zZzFX8nKPUQoWTZPMW&cbiClS_B{Y;bd@f|ZK2?L|^E}(WNpL1rjTgLTkW_5=U5?fs z`K({agPSX-^)(*XeyMkP+BXNI)dQWNHt74Q8^fikqT!BIzVM8Cpl4HOoa=nBNX^ih z>Vm$P3d5 Jz9RDlEA(MI|NaHeYmkn12BT!eJj^R9~b9q_k26<$e z *`=riu>&zJVqn_){d~q5I)>_xQ+LDR;S}t76!NZo-de; zml)uDPX^~9g%nTi)jX^b&+IYw-U%@PKR`rB#k&P?&4upuDwf@C?zeC-8@`zgKO{5I z&aixojZdPH7vdb|^3;6piOwb2=6P!?yzlqC_YcUzt>o<2 zbuxQ2nJ?=@Xa0 PpIA$NM~>)CoAle5!knSpdlBmJ^I4BC-o??xw?eHp%qN%;W2 z`bP%H32bYIm2!!*g?i0N_VgZ_U?I=w8?;4xnxVE>k?Opcm5uv{Y0k37{T;^rwHT1{ zH2U*A$sK9hM5Av|4|E@mdyIZQYn&bP?F-{+E~{`6X+8@p*UboRM4~jszg7@&e+7o~ zEQxrA3_Bl>BmX+e>U}ESBKrSlc>&wh1p3)uRP!y>8$HIfJLxLnpU@(CLFYZg~m zyYF^e^U7zvO#Z;_&htb(%{&2bs<-e8X+DHxeT$_0z*#q`eXv)Y(k|EePFBJH_(3yJ z(8Xj(aU5ewvglf^yMYCD1y5> rh=|NivI`1ej*G!VzmvJgEh%@z z&LZ94Bi*OlW0a_>;h2~~v_+q|f#|9FnYZHZxZbhkU~hP-E#`JKo9biFWgFS_Gv4+n z_Wf(p?PHj4KD{`cHPw!lHJD_bPEHRap_97zN&am=i1cNg%t&z*y`Z5t>Gjzx`tH^} zv!i@|!+@@K%rMr}i)^YMEW z_{wLB 8;