paint-brush
使用 ChatGPT、Google Cloud 和 Python 构建文档分析器经过@shanglun
9,140 讀數
9,140 讀數

使用 ChatGPT、Google Cloud 和 Python 构建文档分析器

经过 Shanglun Wang31m2023/08/05
Read on Terminal Reader

太長; 讀書

我们使用 ChatGPT、Google Cloud 和 React JS 构建强大的 AI 应用程序,可以分析任何语言的文档,并可以回答有关文档的任何问题。对于研究人员和编码人员来说都是很好的资源。
featured image - 使用 ChatGPT、Google Cloud 和 Python 构建文档分析器
Shanglun Wang HackerNoon profile picture
0-item
1-item

当 OpenAI 向公众发布 ChatGPT 时,很少有人,包括 OpenAI 本身的高管,能够预料到公众采用的速度。自此,ChatGPT 取代 TikTok 成为最快突破 1 亿用户的应用程序。各行各业的人们已经找到了使用 ChatGPT 来提高效率的方法,公司也纷纷制定其使用指南。包括许多学术机构在内的一些组织大多对其使用持怀疑态度,而科技公司等其他组织则采取了更为自由的政策,甚至围绕 ChatGPT API 创建应用程序。今天,我们将逐步构建一个这样的应用程序。

目标听众

本文分为三个部分:1)应用程序底层技术的解释,2)应用程序的后端,3)应用程序的前端。如果您可以阅读一些基本的 Python 代码,那么您应该能够轻松地理解前两部分,如果您对 React.js 有一些基本的经验,那么您可以毫无问题地理解第三部分。

应用程序

我们今天构建的应用程序对于经常使用外语资源进行研究的任何人来说都是有用的。一个很好的例子是宏观经济学家,他们经常需要阅读用外语发布的政府报告。有时,这些报告可以复制粘贴到机器翻译服务中,但有时它们会以不可搜索的 PDF 形式发布。在这些情况下,研究人员将需要聘请人工翻译,但资源限制极大地限制了可翻译的报告数量。使问题进一步复杂化的是,这些报告可能非常长且读起来乏味,这使得翻译和分析成本高昂且耗时。


我们的应用程序将通过结合我们可以使用的多种人工智能和机器学习工具(OCR、机器翻译和大型语言模型)来简化此过程。我们将使用 OCR 从 PDF 中提取原始文本内容,使用机器翻译将其翻译成英语,并使用大型语言模型分析翻译后的提取。


对于今天的应用,我们将查看日本政府的 PDF 出版物,即日本文部科学省的创新白皮书。虽然 PDF 本身是可搜索的并且可以复制到翻译引擎中,但我们将假装 PDF 不可搜索,以展示应用程序中使用的技术。原始文档可以在这里找到。


如果您现在只想构建应用程序,请随意跳过下一部分。但是,如果您想更好地了解我们将在本文中使用的各种技术,下一节将为您提供一些背景知识。

底层技术

我们将使用的第一项技术是 OCR(光学字符识别),它是最早向公众开放的商业机器学习应用程序之一。 OCR 模型和应用程序旨在拍摄照片或图像,然后从图像中识别并提取文本信息。乍一看这似乎是一个简单的任务,但问题实际上相当复杂。例如,字母可能会稍微模糊,从而难以做出明确的识别。该字母也可能以不寻常的方向排列,这意味着机器学习模型必须识别垂直和颠倒的文本。尽管存在这些挑战,研究人员仍然开发了许多快速且强大的 OCR 模型,并且其中许多模型的成本相对较低。对于今天的应用程序,我们将使用 Google 的 Cloud Vision 模型,我们可以使用 Google Cloud API 访问该模型。


我们将使用的下一个技术是机器翻译。这和 OCR 一样,是一个极其困难的机器学习问题。人类语言充满了特质和错综复杂的上下文,这使得计算机处理和理解特别困难。汉语和英语等不同语言对之间的翻译往往会产生特别不准确和幽默的结果,因为这些语言本身结构不同,需要截然不同的数字化和嵌入策略。然而,尽管存在这些挑战,研究人员仍然开发出了强大而复杂的模型,并将其普遍使用。今天,我们将使用 Google 的翻译 API,它是最好、使用最广泛的机器翻译工具之一。


我们将使用的最后一项机器学习技术是 LLM(大型语言模型),它对消费者人工智能来说是革命性的。法学硕士能够理解人类自然语言的结构,并能够利用大量数据来生成详细且内容丰富的响应。该技术仍然存在许多限制,但其灵活性和数据处理能力激发了许多用于模型交互的新技术的创建。其中一种技术称为“提示工程” ,用户可以巧妙地对模型进行措辞和结构化的输入或提示,以获得所需的结果。在今天的应用中,我们将使用ChatGPT的API和一些简单的提示工程来帮助我们分析翻译后的报告。

后端

云服务设置

在开始编写应用程序之前,我们需要首先注册服务。


由于 ChatGPT 的 API 网站不断变化,我们将无法提供注册 ChatGPT API 的确切步骤。但是,您应该在API 文档网站上找到易于遵循的说明。只需继续操作,直到获得 API 密钥,我们需要调用 ChatGPT API。


Google Cloud 稍微复杂一些,但注册起来也相对简单。只需前往Google Cloud 控制台并按照设置项目的说明进行操作即可。进入项目后,您将需要导航到 IAM 和管理控制台,并创建一个服务帐户。虽然 Google Cloud 控制台一直在变化,但您应该只需在网页上搜索“IAM”和“服务帐户”即可导航界面。创建服务帐户后,您将需要将私钥文件的副本下载到您的计算机。您还需要复制私钥字符串,因为翻译 REST API 使用私钥字符串而不是密钥文件。


在我们完成 Google Cloud 设置之前,您需要启用机器视觉 API,您可以从主控制台页面执行此操作。只需搜索机器视觉 API,单击 Google 中的产品,然后激活 API。您还需要创建一个存储桶来保存我们将用于该项目的数据。

Python设置

现在我们已经注册了适当的服务,我们准备开始用 Python 编写我们的应用程序。首先,我们需要将必需的包安装到我们的 Python 环境中。


Pip install google-cloud-storage google-cloud-vision openai requests


安装完成后,让我们创建一个新文件夹,下载 PDF 文件,并在同一文件夹中创建一个新的 Python 文件。我们将其命名为 document_analyze.py。我们首先导入必要的包:


 import requests Import openai from google.cloud import vision from google.cloud import storage Import os Import json Import time Import shutil

然后我们可以进行一些基本设置,以便我们的应用程序可以使用我们刚刚注册的云服务:

 os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = [Path to your Google Cloud key file] openai.api_key = [your openAI API key]

准备好这些凭据后,您现在应该能够从 Python 脚本访问 Google Cloud 和 ChatGPT API。我们现在可以编写将为我们的应用程序提供所需功能的函数。

识别码

现在我们可以开始构建一些将成为应用程序构建块的功能。让我们从 OCR 功能开始:


 # upload the file to google cloud storage bucket for further processing def upload_file(file_name, bucket_name, bucket_path): client = storage.Client() bucket = client.get_bucket(bucket_name) blob = bucket.blob(bucket_path) blob.upload_from_filename(file_name) # kick off the OCR process on the document. This is asynchronous because OCR can take a while def async_detect_document(gcs_source_uri, gcs_destination_uri): client = vision.ImageAnnotatorClient() input_config = vision.InputConfig(gcs_source=vision.GcsSource(uri=gcs_source_uri), mime_type= 'application/pdf') output_config = vision.OutputConfig( gcs_destination=vision.GcsDestination(uri=gcs_destination_uri), batch_size=100 ) async_request = vision.AsyncAnnotateFileRequest( features=[vision.Feature(type_=vision.Feature.Type.DOCUMENT_TEXT_DETECTION)], input_config=input_config, output_config=output_config ) operation = client.async_batch_annotate_files(requests=[async_request]) def check_results(bucket_path, prefix): storage_client = storage.Client() bucket = storage_client.get_bucket(bucket_path) blob_list = list(bucket.list_blobs(prefix=prefix)) blb = [b for b in blob_list if 'output-' in b.name and '.json' in b.name] return len(blb) != 0 # download the OCR result file def write_to_text(bucket_name, prefix): bucket = storage.Client().get_bucket(bucket_name) blob_list = list(bucket.list_blobs(prefix=prefix)) if not os.path.exists('ocr_results'): os.mkdir('ocr_results') for blob in blob_list: if blob.name.endswith('.json'): with open(os.path.join('ocr_results', blob.name), 'w') as fp_data: fp_data.write(blob.download_as_string().decode('utf-8')) def delete_objects(bucket, prefix): bucket = storage.Client().get_bucket(bucket) blob_list = list(bucket.list_blobs(prefix=prefix)) for blob in blob_list: blob.delete() print('Blob', blob.name, 'Deleted')


让我们详细研究一下每个函数的作用。


upload_file函数是一个从 Google Cloud Storage 容器中获取存储桶并将文件上传到其中的函数。 Google Cloud 出色的抽象使得编写此函数变得非常容易。


async_detect_document函数异步调用 Google Cloud 的 OCR 函数。由于 Google Cloud 中可用的选项数量较多,我们必须实例化一些配置对象,但这实际上只是让 Google Cloud 知道源文件在哪里以及输出应该写入哪里。 batch_size变量设置为 100,因此 google cloud 将一次处理文档 100 页。这减少了写入的输出文件的数量,从而使处理更容易。另一个需要注意的重要事项是调用是异步的,这意味着 Python 脚本的执行将继续,而不是等待处理完成。虽然这对这个特定阶段没有太大影响,但稍后当我们将 Python 代码转换为 Web API 时,它会变得更加有用。


check_results函数是一个简单的云存储函数,用于检查处理是否完成。由于我们异步调用 OCR 函数,因此需要定期调用该函数来查看结果文件是否存在。如果有结果文件,该函数将返回true,我们可以继续分析。如果没有结果文件,函数将返回 false,我们将继续等待,直到处理完成。


write_to_text函数将结果文件下载到磁盘以进行进一步处理。该函数将迭代存储桶中具有特定前缀的所有文件,检索输出字符串,并将结果写入本地文件系统。


delete_objects函数虽然与 OCR 并不严格相关,但会清理上传的文件,以便系统不会在 Google Cloud Storage 中保留不必要的工件。


现在我们已经完成了 OCR 调用,让我们看看机器翻译代码!

机器翻译代码

现在我们可以定义翻译函数:


 # detect the language we're translating from def detect_language(text): url = 'https://translation.googleapis.com/language/translate/v2/detect' data = { "q": text, "key": [your google cloud API key] } res = requests.post(url, data=data) return res.json()['data']['detections'][0][0]['language'] # translate the text def translate_text(text): url = 'https://translation.googleapis.com/language/translate/v2' language = detect_language(text) if language == 'en': return text data = { "q": text, "source": language, "target": "en", "format": "text", "key": [your google cloud API key] } res = requests.post(url, data=data) return res.json()['data']['translations'][0]['translatedText']


这些功能相当简单。 detect_language函数调用语言检测API 来确定后续translate_text调用的源语言。虽然我们知道 PDF 是用日语编写的,但最佳实践仍然是运行语言检测,以便应用程序可以处理其他语言。 translate_text函数只是使用 Google Translation API 将文本从检测到的源语言翻译为英语,但如果它确定源语言已经是英语,则会跳过翻译。

ChatGPT 代码和提示工程

最后,我们调用 ChatGPT:

 def run_chatgpt_api(report_text): completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": ''' Consider the following report: --- %s --- 1. Summarize the purpose of the report. 2. Summarize the primary conclusion of the report. 3. Summarize the secondary conclusion of the report 4. Who is the intended audience for this report? 5. What other additional context would a reader be interested in knowing? Please reply in json format with the keys purpose, primaryConclusion, secondaryConclusion, intendedAudience, and additionalContextString. ''' % report_text}, ] ) return completion.choices[0]['message']['content']


请注意,Python 调用是一个相对简单的 API 调用,但提示的编写方式是产生特定结果:


  1. 提示提供报告文本作为上下文,因此 ChatGPT 可以轻松分析报告。文本使用虚线划分,可以轻松识别报告的结束位置和问题的开始位置。

  2. 这些问题是列举出来的,而不是以段落的形式陈述的。因此,响应可能遵循类似的结构。与 ChatGPT 以段落格式回复相比,枚举结构使结果更容易用代码解析。

  3. 提示指定回复的格式,在本例中为 JSON 格式。 JSON 格式非常容易用代码处理。

  4. 提示指定 JSON 对象的键,并选择非常容易与问题关联的键。

  5. 这些键还使用 ChatGPT 应该识别的常用约定(驼峰命名法)。 JSON 键是完整的单词而不是缩写。这使得 ChatGPT 更有可能在响应中使用实际密钥,因为 ChatGPT 习惯于在处理过程中进行“拼写更正”。

  6. extraContextString 为 ChatGPT 提供了传递附加信息的出口。这利用了大型语言模型的自由形式分析能力。

  7. 提示使用了当前主题的技术讨论中常见的措辞。正如您可能从 API 调用的结构中猜测的那样,ChatGPT 的“真正”目标不一定是提供提示的答案,而是预测对话中的下一行。

    因此,如果您的提示措辞类似于表面级讨论中的一行,您可能会得到表面级答案,而如果您的提示措辞类似于专家讨论中的一行,您更有可能收到专家结果。这种效应对于数学或技术等科目尤其明显,但在这里也相关。


以上是称为“提示工程”的新工作中的基本技术,其中用户构建提示以获得特定结果。对于像 ChatGPT 这样的大型语言模型,精心设计提示可以显着提高模型的有效性。与编码有很多相似之处,但即时工程需要工程师更多的直觉和模糊推理。随着像 ChatGPT 这样的工具越来越深入到工作场所,工程将不再是一项纯粹的技术工作,而是一项哲学工作,工程师应该注意培养他们的直觉和大型语言模型的“心理理论”,以推动他们的效率。

把它们放在一起

现在让我们把它们放在一起。如果您遵循了上一节,那么下面的代码块应该相当容易理解。


 bucket = [Your bucket name] upload_file_name = [the name of the PDF in the Google Cloud bucket] upload_prefix = [the prefix to use for the analysis results] pdf_to_analyze = [path to the PDF to analyze] upload_file(pdf_to_analyze, bucket, upload_file_name) async_detect_document(f'gs://{bucket}/{upload_file_name}', f'gs://{bucket}/{upload_prefix}') while not check_results(bucket, upload_prefix): print('Not done yet... checking again') time.sleep(5) If __name__ == '__main__': write_to_text(bucket, upload_prefix) all_responses = [] for result_json in os.listdir('ocr_results'): with open(os.path.join('ocr_results', result_json)) as fp_res: response = json.load(fp_res) all_responses.extend(response['responses']) texts = [a['fullTextAnnotation']['text'] for a in all_responses] translated_text = [translate_text(t) for t in texts] print('Running cleanup...') delete_objects(bucket, upload_file_name) delete_objects(bucket, upload_prefix) shutil.rmtree('ocr_results') print('Running Analysis...') analysis = run_chatgpt_api('\n'.join(translated_text)) print('=== purpose ====') print(analysis_res['purpose']) print() print('==== primary conclusion =====') print(analysis_res['primaryConclusion']) print() print('==== secondary conclusion =====') print(analysis_res['secondaryConclusion']) print() print('==== intended audience ====') print(analysis_res['intendedAudience']) print() print('===== additional context =====') print(analysis_res['additionalContextString'])


我们将报告上传到存储桶,启动 OCR,然后等待 OCR 完成。然后我们下载 OCR 结果并将其放入列表中。我们翻译结果列表,并将其发送到ChatGPT进行分析,并打印出分析结果。


由于人工智能工具具有不确定性,因此如果运行此代码,您可能会得到类似但不相同的结果。然而,这是我使用上面链接的 PDF 得到的输出:


 Not done yet... checking again Not done yet... checking again Running cleanup... Blob [pdf name] Deleted Blob [OCR Output Json] Deleted Running Analysis... === purpose ==== The purpose of the report is to analyze the current status and issues of Japan's research capabilities, discuss the government's growth strategy and investment in science and technology, and introduce the initiatives towards realizing a science and technology nation. ==== primary conclusion ===== The primary conclusion of the report is that Japan's research capabilities, as measured by publication index, have been declining internationally, raising concerns about a decline in research capabilities. ==== secondary conclusion ===== The secondary conclusion of the report is that the Kishida Cabinet's growth strategy emphasizes becoming a science and technology nation and strengthening investment in people to achieve growth and distribution. ==== intended audience ==== The intended audience for this report is government officials, policymakers, researchers, and anyone interested in Japan's research capabilities and science and technology policies. ===== additional context ===== The report focuses on the importance of science, technology, and innovation for Japan's future development and highlights the government's efforts to promote a 'new capitalism' based on a virtuous cycle of growth and distribution. It also mentions the revision to the Basic Law on Science, Technology, and Innovation, the 6th Science, Technology, and Innovation Basic Plan, and the concept of Society 5.0 as the future society Japan aims for. The report suggests that comprehensive knowledge is necessary to promote science, technology, and innovation and emphasizes the importance of transcending disciplinary boundaries and utilizing diverse knowledge.


作为一个日语使用者,我可以验证分析得相当不错!您可以自己进行实验,提供自己的 PDF 并更改问题以满足您的需求。现在,您拥有一个强大的 AI 工具来翻译和总结您遇到的任何外语 PDF。如果您是本文的目标读者,我希望您对刚刚出现的可能性感到相当兴奋!


完整的后端代码可以在我的GitHub上找到

前端

我们已经构建了一个功能强大的应用程序,但在当前的形式下,用户必须了解一点 Python 才能最大程度地使用该应用程序。如果您的用户不想阅读或编写任何代码怎么办?在这种情况下,我们将希望围绕这个工具构建一个 Web 应用程序,以便人们可以从舒适的浏览器中访问 AI 的全部功能。

搭建 React 应用程序的脚手架

让我们首先创建一个 React 应用程序。确保已安装 Node,导航到您希望应用程序代码所在的文件夹,然后运行 create-react-app 脚本并安装一些基本包:


npx create-react-app llm-frontend


进而:


 cd llm-frontend npm install bootstrap react-bootstrap axios


如果我们正在开发一个成熟的应用程序,我们还希望安装包来处理状态管理和路由,但这超出了本文的范围。我们将简单地编辑 App.jsx 文件。


执行npm run start启动开发服务器,您的浏览器应该打开一个页面http://localhost:3000 。保留该页面,然后在您喜欢的文本编辑器中打开 App.jsx 文件。你应该看到这样的东西:


 import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;


继续删除样板代码并将其替换为一些基本的 Bootstrap 组件。


 import React, {useState, useEffect} from 'react'; Import axios from 'axios'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import 'bootstrap/dist/css/bootstrap.min.css'; function App() { return ( <Container> <Row> <Col md={{ span: 10, offset: 1 }}> Main Application Here </Col> </Row> </Container> ); } export default App;


保存应用程序,您应该会在浏览器中看到它更新。现在看起来很稀疏,但不用担心,我们很快就会解决这个问题。

应用组件

为了使该应用程序正常工作,我们需要四个主要组件:用于上传文件的文件选择器、用于显示翻译和摘要的结果显示、用于用户提出自己的问题的文本输入以及用于显示的结果显示用户问题的答案。


我们现在可以构建更简单的组件,并为更复杂的界面添加占位符。在我们这样做的同时,让我们创建用于为界面提供支持的数据容器:


 import React, {useState, useEffect} from 'react'; import axios from 'axios'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import Accordion from 'react-bootstrap/Accordion'; import Form from 'react-bootstrap/Form'; import ListGroup from 'react-bootstrap/ListGroup'; import 'bootstrap/dist/css/bootstrap.min.css'; const ResultDisplay = ({ initialAnalysis, userQuestion, setUserQuestion, userQuestionResult, userQuestionMessage, userQuestionAsked }) => { return <Row> <Col> <Row style={{marginTop: '10px'}}> <Col md={{ span: 10, offset: 1 }}> <Accordion defaultActiveKey="0"> <Accordion.Item eventKey="0"> <Accordion.Header>Analysis Result</Accordion.Header> <Accordion.Body> {initialAnalysis.analysis} </Accordion.Body> </Accordion.Item> <Accordion.Item eventKey="1"> <Accordion.Header>Raw Translated Text</Accordion.Header> <Accordion.Body> {initialAnalysis.translatedText} </Accordion.Body> </Accordion.Item> <Accordion.Item eventKey="2"> <Accordion.Header>Raw Source Text</Accordion.Header> <Accordion.Body> {initialAnalysis.rawText} </Accordion.Body> </Accordion.Item> </Accordion> </Col> </Row> <Row style={{marginTop: '10px'}}> <Col md={{ span: 8, offset: 1 }}> <Form.Control type="text" placeholder="Additional Questions" value={userQuestion} onChange={e => setUserQuestion(e.target.value)} /> </Col> <Col md={{ span: 2 }}> <Button variant="primary">Ask</Button> </Col> </Row> <Row><Col>{userQuestionMessage}</Col></Row> <Row style={{marginTop: '10px'}}> <Col md={{span: 10, offset: 1}}> {userQuestionResult && userQuestionAsked ? <ListGroup> <ListGroup.Item> <div><b>Q:</b> {userQuestionAsked}</div> <div><b>A:</b> {userQuestionResult}</div></ListGroup.Item> </ListGroup>: ''} </Col> </Row> </Col> </Row> } function App() { const [file, setFile] = useState(null); const [haveFileAnalysisResults, setHaveFileAnalysisResults] = useState(false); const [message, setMessage] = useState(''); const [userQuestionMessage, setUserMessage] = useState(''); const [initialAnalysis, setInitialAnalysis] = useState({analysis: '', translatedText: '', rawText: ''}); const [userQuestion, setUserQuestion] = useState(''); const [userQuestionResult, setUserQuestionResult] = useState(''); const [userQuestionAsked, setUserQuestionAsked] = useState(''); return ( <Container> <Row> <Col md={{ span: 8, offset: 1 }}> <Form.Group controlId="formFile"> <Form.Label>Select a File to Analyze</Form.Label> <Form.Control type="file" onChange={e => setFile(e.target.files[0])} /> </Form.Group> </Col> <Col md={{span: 2}}> <div style={{marginTop: '30px'}}><Button variant="primary" >Analyze</Button></div> </Col> <Col md={12}>{message}</Col> </Row> {haveFileAnalysisResults? <ResultDisplay initialAnalysis={initialAnalysis} userQuestion={userQuestion} setUserQuestion={setUserQuestion} userQuestionResult={userQuestionResult} userQuestionMessage={userQuestionMessage} userQuestionAsked={userQuestionAsked} />: ''} </Container>); }


虽然有相当多的新代码,但新代码并没有什么复杂或突破性的地方。代码只是基于React-Bootstrap组件的基础数据录入和展示界面。


保存文件,您应该会看到浏览器更新以显示文件上传界面。

尝试一下变量,您应该会看到这样的界面。这将是我们应用程序的前端界面。


现在我们已经编写了基本的前端接口,让我们编写将应用程序连接到我们(尚未编写的)API 的函数。这些函数都将在 app 对象中定义,因此它将可以访问所有 React hooks。如果您不完全确定这些函数应该放在哪里,您可以参考GitHub上托管的完整代码。


首先,让我们编写几个实用函数来向用户传递消息。


 const flashMessageBuilder = (setMessage) => (message) => { setMessage(message); setTimeout(() => { setMessage(''); }, (5000)); } const flashMessage = flashMessageBuilder(setMessage); const flashUserQuestionMessage = flashMessageBuilder(setUserQuestionMessage);


正如您所看到的,这些都是简单的函数,它们在适当的位置显示一条消息,并创建一个 5 秒后删除该消息的时间。这是一个简单的 UI 功能,但使应用程序感觉更加动态和可用。


接下来,让我们编写函数来分析文件并检查结果。


 const pollForResults = (batchId) => { flashMessage('Checking for results...'); return new Promise((resolve, reject) => { setTimeout(() => { axios.post('http://localhost:5000/check_if_finished', {batchId}) .then(r => r.data) .then(d => { // the result should have a "status" key and a "result" key. if (d.status === 'complete') { resolve(d); // we're done! } else { resolve(pollForResults(batchId)); // wait 5 seconds and try again. } }).catch(e => reject(e)); }, 5000); }) } const analyzeFile = () => { if (file === null) { flashMessage('No file selected!'); return; } flashMessage('Uploading file...'); const formData = new FormData(); formData.append("file", file); axios.post("http://localhost:5000/analyze_file", formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) .then(d => { // the result should contain a batchId that we use to poll for results. flashMessage('File upload success, waiting for analysis results...'); return pollForResults(d.batchId); }) .then(({analysis, translatedText, rawText}) => { // the result should contain the initial analysis results with the proper format. setInitialAnalysis({analysis, translatedText, rawText}); setHaveFileAnalysisResults(true); // show the results display now that we have results }) .catch(e => { console.log(e); flashMessage('There was an error with the upload. Please check the console for details.'); }) }


又是一组非常简单的函数。 analyzeFile 函数将文件发送到analyze_file 端点进行分析。 API 将为其提供一个批次 ID,用于通过 pollForResults 函数检查结果。如果分析完成,pollForResults 函数将命中 check_if_finished 端点并返回结果;如果分析仍在处理中,则等待 5 秒。然后analyzeFile“线程”将继续执行,将数据放入适当的位置。


最后,让我们编写一个让用户提出自由问题的函数:


 const askUserQuestion = () => { flashUserQuestionMessage('Asking user question...') axios.post('http://localhost:5000/ask_user_question', { text: initialAnalysis.translatedText, userQuestion }).then(r => r.data) .then(d => { setUserQuestionResult(d.result); setUserQuestionAsked(userQuestion); }).catch(e => { console.log(e); flashUserQuestionMessage('There was an issue asking the question. Please check the console for details'); }); }


同样,这是一个相当简单的函数。我们提供翻译后的文本以及用户问题,以便我们的 API 可以构建 ChatGPT 提示。然后将结果推送到适当的数据容器进行显示。


我们已经基本完成了 React 应用程序,但在开始编写 API 之前,让我们再进行一项外观更改。现在,分析结果显示配置为将 ChatGPT 分析显示为字符串。然而,ChatGPT 分析实际上是一个 JSON 数据对象,因此为了正确显示它以供人类使用,我们需要向显示对象添加一些格式。将第一个 Accordion 项替换为以下代码:


 <Accordion.Item eventKey="0"> <Accordion.Header>Analysis Result</Accordion.Header> <Accordion.Body> <h6>Purpose</h6> <p>{analysis.purpose}</p> <h6>Primary Conclusion</h6> <p>{analysis.primaryConclusion}</p> <h6>Secondary Conclusion</h6> <p>{analysis.secondaryConclusion}</p> <h6>Intended Audience</h6> <p>{analysis.intendedAudience}</p> <h6>Additional Context</h6> <p>{analysis.additionalContextString}</p> </Accordion.Body> </Accordion.Item>


现在前端已经完成,让我们转到 Python 代码并构建后端。

烧瓶API

首先,让我们安装 Flask,我们将用它来编写后端。


Pip install flask flask-cors


Flask 是一个用于构建 Web 应用程序和 Web API 的简单框架。界面非常简单,运行服务器非常简单:


 from flask import Flask, request, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/') def hello_world(): return "Hello from flask!" if __name__ == '__main__': app.run(debug=True)


运行此文件并在浏览器中导航到http://localhost:5000 ,您应该看到“Hello from Flask!”信息。


现在我们可以开始构建 API 功能。让我们首先导入所需的函数并定义一些常量:


 from flask import Flask, request, jsonify from flask_cors import CORS import uuid import os import json from document_analyze import upload_file, async_detect_document, check_results, \ write_to_text, translate_text, delete_objects, run_chatgpt_api,\ ask_chatgpt_question app = Flask(__name__) CORS(app) BUCKET = '[YOUR BUCKET NAME]'


此代码假设您的服务器代码与我们之前编写的 document_analyze.py 文件位于同一文件夹中,但您可以选择您喜欢的任何目录结构,只要服务器代码可以从 document_analyze.py 找到并导入即可。让我们为文件上传端点编写处理程序:


 @app.route('/analyze_file', methods=['POST']) def analyze_file(): file_to_analyze = request.files['file'] batch_name = str(uuid.uuid4()) local_file_path = 'uploads/%s.pdf' % batch_name cloud_file_path = '%s.pdf' % batch_name file_to_analyze.save(local_file_path) upload_file(local_file_path, BUCKET, cloud_file_path) async_detect_document( f'gs://{BUCKET}/{cloud_file_path}', f'gs://{BUCKET}/{batch_name}') return jsonify({ 'batchId': batch_name })


如您所见,此函数获取上传的文件,将其发送到 Google Cloud Storage,并启动 OCR 过程。它看起来应该很熟悉,但这里有一些值得指出的小变化。首先,该文件由 UUID 标识,该 UUID 也用作批处理名称。这可以避免多次调用 API 时可能出现的潜在冲突问题,并且还可以唯一标识特定分析批次中使用的所有文件,从而更轻松地检查进度并执行清理。


现在让我们编写处理程序,让应用程序检查分析是否完成。


 @app.route('/check_if_finished', methods=['POST']) def check_if_finished(): batch_name = request.json['batchId'] if not check_results(BUCKET, batch_name): return jsonify({ 'status': 'processing' }) write_to_text(BUCKET, batch_name) all_responses = [] for result_json in os.listdir('ocr_results'): if result_json.endswith('json') and result_json.startswith(batch_name): result_file = os.path.join('ocr_results', result_json) with open(os.path.join('ocr_results', result_json)) as fp_res: response = json.load(fp_res) all_responses.extend(response['responses']) os.remove(result_file) txts = [a['fullTextAnnotation']['text'] for a in all_responses] translated_text = [translate_text(t) for t in txts] print('Running cleanup...') delete_objects(BUCKET, batch_name) os.remove('uploads/%s.pdf' % batch_name) analysis = run_chatgpt_api('\n'.join(translated_text)) analysis_res = json.loads(analysis) return jsonify({ 'status': 'complete', 'analysis': analysis, 'translatedText': translated_text, 'rawText': '\n'.join(txts) })


同样,这看起来应该很熟悉。我们首先检查 OCR 是否完成,如果 OCR 未完成,我们只需返回一条消息,说明该批次仍在处理中。如果 OCR 完成,我们将继续分析,下载 OCR 结果并运行翻译和 ChatGPT 管道。我们还确保在分析完成后清理源文件,以避免产生不必要的存储成本。我们将结果打包到最终结果对象中,其中包含 ChatGPT 分析 JSON、翻译文本和 OCR 提取的原始文本。


虽然自定义问题后端是一项新功能,但它相当简单。首先,我们要定义函数来提出自定义问题:


 def ask_chatgpt_question(report_text, question_text): completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": ''' Consider the following report: --- %s --- Answer the following question: %s ''' % (report_text, question_text)}, ] ) return completion.choices[0]['message']['content']


现在我们可以导入函数并定义 API 端点:


 @app.route('/ask_user_question') def ask_user_question(): report_text = request.json['text'] user_question = request.json['userQuestion'] response = ask_chatgpt_question(report_text, user_question) return jsonify({ 'result': response })


现在我们已经编写了 API,让我们从前端测试它。继续通过前端上传文件。你应该看到这样的东西:

上传文件并点击“分析”按钮后


稍等片刻,您应该会在 WebApp 上看到结果。

基本结果

环顾四周,你也会看到翻译后的文本:

翻译文本

以及原始源文本:

原始文本

现在我们来测试一下自定义问题界面:

询问有关文档的问题

很不错!这样,我们就成功地围绕我们的 AI 工具构建了一个 React.js 应用程序!

结论

在今天的文章中,我们构建了一个应用程序,该应用程序利用了当前市场上一些最强大的人工智能工具。虽然这个特定的应用程序旨在解析和总结外语 PDF,但类似的技术可以用于在许多领域开发强大的革命性应用程序。


我希望这篇文章能够启发您编写自己的人工智能驱动的应用程序。如果您想与我联系并谈谈您对人工智能应用的看法,我很乐意听取您的意见。如果您正在寻找构建类似的应用程序,请随时参考我的Github页面上托管的代码,您可以在其中找到前端后端的存储库


如果您想使用此应用程序而不必自己构建它,我已经构建并托管了该应用程序的更复杂版本以供一般使用。如果您想访问此应用程序,请联系我们,我们可以提供对该网站的访问权限。


请留意后续文章,我们将在其中构建并运行我们自己的法学硕士实例。文章发表后,我们将在此处发布该文章的链接。敬请关注!