基于AI 代理的PDF文档总结

本文的目的是使用 AI 代理,以尽可能低的成本总结任何 PDF 文档。

基于AI 代理的PDF文档总结

AI 代理因其能够处理大量数据、做出明智的决策并自主执行任务而对推理和行动有用。以下是可能使用它们的原因:

  1. 自动化决策——AI 代理可以分析复杂场景,识别模式,并在没有人工干预的情况下做出最佳决策。
  2. 可扩展性——它们可以高效地处理大规模问题,同时处理多个请求。
  3. 适应性和学习能力——先进的 AI 代理,特别是那些具有强化学习或微调 LLM 的,可以适应新数据,随着时间的推移提高其推理能力。
  4. 多步骤任务执行——它们可以分解复杂的任务,计划行动并系统地执行它们。
  5. 与 API 和系统的交互——AI 代理可以集成到外部工具中,检索数据并自动执行工作流程。
  6. 增强生产力——它们减少了手动工作量,使人类有更多时间从事战略性工作。

1、目标

  • 使用 AI 代理,以尽可能低的成本总结任何 PDF 文档。因此,如果 PDF 是纯文本,我们可以使用本地工具进行总结,或者如果包含图表等更复杂的内容,则使用 LLM 如 Gemini Gemini。
  • 通常,我们会使用 AssistantAgent 或其他由 LLM 支持的东西来做出决定。然而,在这种情况下,我们将使用一个自定义代理,不使用 LLM 来根据 PDF 内容的一些自定义逻辑做出决定,从而限制初始 LLM 使用成本。

2、系统架构

关键组件:

  • AutoGen (v0.4) —— AI 代理框架。为了这次演示我使用了 AutoGen,但也可以使用其他许多替代方案,因为这个演示不依赖于任何特定的原生 AutoGen 功能。
  • Google Gemini 1.5 —— 用于图像转文本摘要的 Google LLM(付费选项)
  • MarkItDown —— 用于纯文本摘要的 Python 库(免费选项)

代理架构:

3、代码实现

定义代理:

def get_filename_without_extension(file_path):  
    return os.path.splitext(os.path.basename(file_path))[0]  
  
# 定义代理  
class UserProxyAgent(BaseChatAgent):  
    def __init__(self, name: str) -> None:  
        super().__init__(name, "A human user participating in the chat.")  
  
    @property  
    def produced_message_types(self) -> List[type[ChatMessage]]:  
        return [TextMessage]  
  
    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:  
        user_input = await asyncio.get_event_loop().run_in_executor(None, input, "Enter source PDF document Path:")  
        print("input:",user_input)  
        return Response(chat_message=TextMessage(content=user_input, source=self.name))  
  
    async def on_reset(self, cancellation_token: CancellationToken) -> None:  
        print("User Proxy Reset")  
  
# 自定义 UserProxyAgent 以与 Gemini 交互  
class GeminiAgent(BaseChatAgent):  
    def __init__(self, name: str, output_dir: str, mime_type : str = None):  
        super().__init__(name=name, description="An agent that converts images to text.")  
        self._output_dir = output_dir  
        self._mime_type = mime_type  
  
    @property  
    def produced_message_types(self) -> Sequence[type[ChatMessage]]:  
        return (TextMessage,)  
          
    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:  
        # 使用 Google Cloud SDK 调用 Gemini  
        if not messages:  
            return Response(chat_message=StopMessage(content="No File Name received!", source=self.name))  
        last_message = messages[-1].content.strip()  
        filePath = last_message  
        print("file:",filePath)  
        response = await self.process(filePath=filePath, mime_type=self._mime_type)  
        # 返回从 Gemini 生成的响应  
        final_msg = TextMessage(content=f"created:{filePath}", source=self.name)  
        return Response(chat_message=final_msg)  
  
    async def on_reset(self, cancellation_token: CancellationToken) -> None:  
        pass  
      
    async def process(self, filePath: str, mime_type: str = None) -> str:  
        try:  
            model = GenerativeModel("gemini-1.5-flash-002")  
            if(mime_type == "application/pdf"):  
                print("summarise pdf...")              
                encoded=self.encode_pdf(filePath)  
                #print("encoded", base64.b64decode(encoded))  
                attachment_file = Part.from_data( mime_type="application/pdf",  
                                        data=encoded)  
                prompt = "Can you summarise content including images from the attached pdf in markdown?"  
            else:  
                print("summarise image...")    
                attachment_file = Part.from_image(Image.load_from_file(filePath))  
                prompt = "Describe this image?"  
            # 查询模型  
            print("total_tokens: ", model.count_tokens([attachment_file, prompt]))  
            response = model.generate_content([attachment_file, prompt])  
            print("usage_metadata: ", response.usage_metadata)  
            filename = os.path.join(self._output_dir, get_filename_without_extension(filePath))+"_gemini.md"  
            with open(filename, "w", encoding="utf-8") as file:  
                file.write(response.text)  
            return f"created:{filename}"  
        except Exception as e:  
            print("Exception : ", traceback.format_exc())  
            return f"Error calling Vertex AI: {str(e)}"  
          
    def encode_pdf(self, pdf_path):  
        return Path(pdf_path).read_bytes()  
  
class MarkItDownAgent(BaseChatAgent):  
    def __init__(self, name, output_dir: str):  
        super().__init__(name, "An agent to convert to Mark Down output.")  
        self._output_dir = output_dir  
  
    @property  
    def produced_message_types(self) -> Sequence[type[ChatMessage]]:  
        return (TextMessage,)  
  
    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:  
        if not messages:  
            return Response(chat_message=StopMessage(content="No File Name received!", source=self.name))  
        last_message = messages[-1].content.strip()  
        filePath = last_message  
        return await self.process(filePath)  
  
    async def process(self, filePath: str) -> Response:  
        inner_messages: List[ChatMessage] = []  
        md = MarkItDown()  
        inner_messages.append(TextMessage(content=f'Will Summarise Free option using MarkDown...', source=self.name))  
        result = md.convert(filePath)  
        filename = os.path.join(self._output_dir, get_filename_without_extension(filePath))+"_markitdown.md"  
        print("content:",result.text_content)  
        with open(filename, "w", encoding="utf-8") as file:  
            file.write(result.text_content)  
        #print(result.text_content)  
        final_msg = TextMessage(content=f"created:{filename}", source=self.name)  
        inner_messages.append(final_msg)  
        return Response(chat_message=final_msg)  
  
    async def on_reset(self, cancellation_token: CancellationToken) -> None:  
        pass  
  
class DocumentTriageJobAgent(BaseChatAgent):  
    def __init__(self, name: str):  
        super().__init__(name, "An agent to decide on how which agent to use for processing document.")  
  
    @property  
    def produced_message_types(self) -> Sequence[type[ChatMessage]]:  
        return (TextMessage,)  
  
    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:  
        if not messages:  
            return Response(chat_message=StopMessage(content="No File Name received!", source=self.name))  
        last_message = messages[-1].content.strip()  
        filePath = last_message  
        return await self.process(filePath)  
  
    async def process(self, filePath: str) -> Response:  
        inner_messages: List[ChatMessage] = []  
        if not(os.path.isfile(filePath)):  
            return Response(chat_message=StopMessage(content="File path provided isn't a valid file!", source=self.name), inner_messages=inner_messages)  
        classifier_result = self.classifier(filePath)  
        inner_messages.append(TextMessage(content=f'pdf classifier_result: {classifier_result}', source=self.name))  
        print("document type:",classifier_result)  
        if(classifier_result=="TEXT"):  
            agent = MarkItDownAgent(name="markdown_agent", output_dir=output_dir)  
        elif (classifier_result=="IMAGE"):  
            agent = GeminiAgent(name="llm_agent", output_dir=output_dir, mime_type="application/pdf")  
        await Console(  
                agent.on_messages_stream(  
                    [TextMessage(content=filePath, source="user")], CancellationToken()  
                )  
            )  
        #print("final:",inner_messages)  
        final_msg = TextMessage(content="Done!", source=self.name)  
        return Response(chat_message=final_msg, inner_messages=inner_messages)  
  
    async def on_reset(self, cancellation_token: CancellationToken) -> None:  
        pass  
  
    def classifier(self, pdf_file):  
        with open(pdf_file,"rb") as f:  
            pdf = fitz.open(f)  
            res = []  
            for page in pdf:  
                img_refs = page.get_image_info(xrefs=True)  
                if img_refs != []:  
                    #print("Page", page.number, "images:", [i["xref"] for i in img_refs])  
                    res.append([i["xref"] for i in img_refs])  
            if  len(res) == 0:  
                return("TEXT")  
            else:  
                return("IMAGE")

运行代理:

# 初始化代理  
async def main():  
    # 初始化代理  
    user_proxy_agent = UserProxyAgent(name="user_proxy_agent")  
    document_triage_agent = DocumentTriageJobAgent(name="document_triage_agent")  
    markitdown_agent = MarkItDownAgent(name="markdown_agent", output_dir=output_dir)  
    llm_agent = GeminiAgent(name="llm_agent", output_dir=output_dir, mime_type="application/pdf")  
    # 定义终止条件  
    termination = TextMentionTermination("Done!") | HandoffTermination(user_proxy_agent) | StopMessageTermination()  
    # 创建聊天组  
    group_chat = RoundRobinGroupChat(  
        participants=[user_proxy_agent, document_triage_agent],  
        termination_condition=termination  
    )  
    # 程序化提供初始消息  
    initial_message = TextMessage(content="Enter Source file Name:", source="user")  
    # 开始聊天  
    stream = group_chat.run_stream(cancellation_token=CancellationToken())  
    # 等待流处理消息  
    await Console(stream)  
nest_asyncio.apply()  
asyncio.run(main())

4、测试 PDF

通过 AI 代理运行了两个 PDF 文件,一个是纯文本,另一个包含图表作为图像。

纯文本摘要示例

纯文本

带图像的文本摘要示例

带有文本的图像示例——图表图像摘要

5、结束语

  • 使用 MarkItDown 时,PDF 的摘要免费,而 LLM 则根据文档内容的大小收费。
  • Google Gemini 1.5 Flash 在文档摘要方面表现良好,尤其是在包含图像内容时。最初,我打算使用 Google 的 DocAI,但 Gemini 更便宜且更容易使用。预计 Google Gemini 2.0 Pro 将会更好。

原文链接:Using AI Agents : Decision based Cost Effective Document Summarization with AutoGen & Google Gemini 1.5 Multi Model

汇智网翻译整理,转载请标明出处