语义检索使用入门

前往 ai.google.dev 查看 试用 Colab 笔记本 在 GitHub 上查看笔记本

概览

大语言模型 (LLM) 无需直接接受新能力训练,即可学习新能力。然而,众所周知,LLM 会“幻觉”当任务是回答自己未接受过训练的问题时。部分原因是 LLM 在训练后不知道事件。此外,追踪 LLM 做出响应的来源也非常困难。对于可靠、可扩展的应用,LLM 必须提供基于事实的响应,并且能够引用其信息来源。

为了克服这些限制,一种常用方法称为“检索增强生成”(RAG),这种方法通过信息检索 (IR) 机制,使用从外部知识库中检索到的相关数据来增强发送到 LLM 的提示。知识库可以是您自己的文档、数据库或 API 语料库。

此笔记本将引导您完成一个工作流,通过使用外部文本语料库扩充 LLM 的知识,并执行语义信息检索来使用语义检索器和归因问题和Generative Language API 的 Answering (AQA) API。

设置

导入 Generative Language API

# Install the Client library (Semantic Retriever is only supported for versions >0.4.0)
pip install -U google.ai.generativelanguage

身份验证

借助 Semantic Retriever API,您可以对自己的数据执行语义搜索。由于是您的数据,因此这需要比 API 密钥更严格的访问权限控制。通过服务账号或您的用户凭据进行 OAuth 身份验证。

本快速入门使用适用于测试环境的简化身份验证方法,而服务账号设置通常更容易开始。对于生产环境,请先了解身份验证和授权,然后再选择适合您应用的访问凭据

使用服务账号设置 OAuth

请按照以下步骤使用服务账号设置 OAuth:

  1. 启用 Generative Language API

  1. 按照文档创建服务账号。

    • 创建服务账号后,生成服务账号密钥。

  1. 依次使用左侧边栏的文件图标和上传图标,上传您的服务账号文件,如下面的屏幕截图所示。

    • 将上传的文件重命名为 service_account_key.json,或更改以下代码中的变量 service_account_file_name

pip install -U google-auth-oauthlib
service_account_file_name = 'service_account_key.json'

from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_file(service_account_file_name)

scoped_credentials = credentials.with_scopes(
    ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/generative-language.retriever'])

使用服务账号凭据初始化客户端库。

import google.ai.generativelanguage as glm
generative_service_client = glm.GenerativeServiceClient(credentials=scoped_credentials)
retriever_service_client = glm.RetrieverServiceClient(credentials=scoped_credentials)
permission_service_client = glm.PermissionServiceClient(credentials=scoped_credentials)

创建语料库

借助 Semantic Retriever API,您可以为每个项目定义最多 5 个自定义文本语料库。您可以在定义语料库时指定以下任一字段:

  • nameCorpus 资源名称 (ID)。最多只能包含 40 个字母数字字符。如果创建时 name 为空,系统会生成一个唯一名称,该名称的长度上限为 40 个字符,带有 display_name 中的前缀以及 12 个字符的随机后缀。
  • display_nameCorpus 的直观易懂的显示名称。不得超过 512 个字符,包括字母数字字符、空格和短划线。
example_corpus = glm.Corpus(display_name="Google for Developers Blog")
create_corpus_request = glm.CreateCorpusRequest(corpus=example_corpus)

# Make the request
create_corpus_response = retriever_service_client.create_corpus(create_corpus_request)

# Set the `corpus_resource_name` for subsequent sections.
corpus_resource_name = create_corpus_response.name
print(create_corpus_response)
name: "corpora/google-for-developers-blog-dqrtz8rs0jg"
display_name: "Google for Developers Blog"
create_time {
  seconds: 1713497533
  nanos: 587977000
}
update_time {
  seconds: 1713497533
  nanos: 587977000
}

获取创建的语料库

使用 GetCorpusRequest 方法以编程方式访问您在上面创建的 Corpusname 参数的值引用了 Corpus 的完整资源名称,并在上面的单元格中设置为 corpus_resource_name。预期格式为 corpora/corpus-123

get_corpus_request = glm.GetCorpusRequest(name=corpus_resource_name)

# Make the request
get_corpus_response = retriever_service_client.get_corpus(get_corpus_request)

# Print the response
print(get_corpus_response)

创建文档

一个 Corpus 最多可包含 10,000 个 Document。您可以在定义文档时指定以下任一字段:

  • nameDocument 资源名称 (ID)。最多只能包含 40 个字符(仅限字母数字字符或短划线)。ID 不能以 破折号。如果创建时名称为空,则系统会从以下位置派生一个唯一名称: display_name 加上 12 个字符的随机后缀。
  • display_name:直观易懂的显示名称。不得超过 512 个字符,包括字母数字字符、空格和短划线。

Document 还支持最多 20 个由用户指定的 custom_metadata 字段(以键值对的形式指定)。自定义元数据可以是字符串、字符串列表或数字。请注意,字符串列表最多可以支持 10 个值,并且数值在 API 中表示为浮点数。

# Create a document with a custom display name.
example_document = glm.Document(display_name="Introducing Project IDX, An Experiment to Improve Full-stack, Multiplatform App Development")

# Add metadata.
# Metadata also supports numeric values not specified here
document_metadata = [
    glm.CustomMetadata(key="url", string_value="https://developers.googleblog.com/2023/08/introducing-project-idx-experiment-to-improve-full-stack-multiplatform-app-development.html")]
example_document.custom_metadata.extend(document_metadata)

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
create_document_request = glm.CreateDocumentRequest(parent=corpus_resource_name, document=example_document)
create_document_response = retriever_service_client.create_document(create_document_request)

# Set the `document_resource_name` for subsequent sections.
document_resource_name = create_document_response.name
print(create_document_response)

获取创建的文档

使用 GetDocumentRequest 方法以编程方式访问您在上面创建的文档。name 参数的值是指文档的完整资源名称,在上方单元格中设置为 document_resource_name。预期格式为 corpora/corpus-123/documents/document-123

get_document_request = glm.GetDocumentRequest(name=document_resource_name)

# Make the request
# document_resource_name is a variable set in the "Create a document" section.
get_document_response = retriever_service_client.get_document(get_document_request)

# Print the response
print(get_document_response)

提取和对文档进行分块

为了提高语义检索期间矢量数据库返回的内容的相关性,请在提取文档时将大型文档拆分为较小的片段或小块

ChunkDocument 的子部分,对于矢量表示和存储,被视为独立单元。一个 Chunk 最多可以有 2043 个令牌。一个 Corpus 最多可以包含 100 万个 Chunk

Document 类似,Chunks 最多也支持 20 个由用户指定的 custom_metadata 字段(以键值对的形式指定)。自定义元数据可以是字符串、字符串列表或数字。请注意,字符串列表最多可以支持 10 个值,并且数值在 API 中表示为浮点数。

本指南使用 Google 的开放源代码 htmlChunker

您还可以使用的其他分块包括 LangChainLlamaIndex

通过 htmlChunker 提取 HTML 和分块

!pip install google-labs-html-chunker

from google_labs_html_chunker.html_chunker import HtmlChunker

from urllib.request import urlopen

获取网站的 HTML DOM。此时,系统会直接读取 HTML, 最好在呈现后获取 HTML,以包含注入 JavaScript 的 HTML 例如 document.documentElement.innerHTML

with(urlopen("https://developers.googleblog.com/2023/08/introducing-project-idx-experiment-to-improve-full-stack-multiplatform-app-development.html")) as f:
  html = f.read().decode("utf-8")

将文本文档拆分为多个段落,然后根据这些段落创建 Chunk。此步骤将创建 Chunk 对象本身,下一部分则将其上传到 Semantic Retriever API。

# Chunk the file using HtmlChunker
chunker = HtmlChunker(
    max_words_per_aggregate_passage=200,
    greedily_aggregate_sibling_nodes=True,
    html_tags_to_exclude={"noscript", "script", "style"},
)
passages = chunker.chunk(html)
print(passages)


# Create `Chunk` entities.
chunks = []
for passage in passages:
    chunk = glm.Chunk(data={'string_value': passage})
    # Optionally, you can add metadata to a chunk
    chunk.custom_metadata.append(glm.CustomMetadata(key="tags",
                                                    string_list_value=glm.StringList(
                                                        values=["Google For Developers", "Project IDX", "Blog", "Announcement"])))
    chunk.custom_metadata.append(glm.CustomMetadata(key="chunking_strategy",
                                                    string_value="greedily_aggregate_sibling_nodes"))
    chunk.custom_metadata.append(glm.CustomMetadata(key = "publish_date",
                                                    numeric_value = 20230808))
    chunks.append(chunk)
print(chunks)

批量创建分块

分批创建分块。每个批量请求最多可以指定 100 个分块。

使用 CreateChunk() 创建单个分块。

# Option 1: Use HtmlChunker in the section above.
# `chunks` is the variable set from the section above.
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))

# Make the request
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

或者,您也可以在不使用 htmlChunker 的情况下制作分块。

# Add up to 100 CreateChunk requests per batch request.
# document_resource_name is a variable set in the "Create a document" section.
chunks = []
chunk_1 = glm.Chunk(data={'string_value': "Chunks support user specified metadata."})
chunk_1.custom_metadata.append(glm.CustomMetadata(key="section",
                                                  string_value="Custom metadata filters"))
chunk_2 = glm.Chunk(data={'string_value': "The maximum number of metadata supported is 20"})
chunk_2.custom_metadata.append(glm.CustomMetadata(key = "num_keys",
                                                  numeric_value = 20))
chunks = [chunk_1, chunk_2]
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))

# Make the request
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

列出 Chunk 并获取状态

使用 ListChunksRequest 方法将所有可用的 Chunk 作为分页列表获取,每页大小上限为 100 个 Chunk,按 Chunk.create_time 的升序排序。如果未指定限制,则最多返回 10 个 Chunk

ListChunksRequest 响应中返回的 next_page_token 作为参数提供给下一个请求,以检索下一页。请注意,在分页时,提供给 ListChunks 的所有其他参数必须与提供页面令牌的调用匹配。

所有 Chunk 都会返回一个 state。在查询 Corpus 之前,使用此方法检查 Chunks 的状态。Chunk 状态包括 - UNSPECIFIEDPENDING_PROCESSINGACTIVEFAILED。您只能查询 ACTIVEChunk

# Make the request
request = glm.ListChunksRequest(parent=document_resource_name)
list_chunks_response = retriever_service_client.list_chunks(request)
for index, chunks in enumerate(list_chunks_response.chunks):
  print(f'\nChunk # {index + 1}')
  print(f'Resource Name: {chunks.name}')
  # Only ACTIVE chunks can be queried.
  print(f'State: {glm.Chunk.State(chunks.state).name}')

注入其他文档

通过 htmlChunker 再添加一个 Document 并添加过滤条件。

# Create a document with a custom display name.
example_document = glm.Document(display_name="How it’s Made: Interacting with Gemini through multimodal prompting")

# Add document metadata.
# Metadata also supports numeric values not specified here
document_metadata = [
    glm.CustomMetadata(key="url", string_value="https://developers.googleblog.com/2023/12/how-its-made-gemini-multimodal-prompting.html")]
example_document.custom_metadata.extend(document_metadata)

# Make the CreateDocument request
# corpus_resource_name is a variable set in the "Create a corpus" section.
create_document_request = glm.CreateDocumentRequest(parent=corpus_resource_name, document=example_document)
create_document_response = retriever_service_client.create_document(create_document_request)

# Set the `document_resource_name` for subsequent sections.
document_resource_name = create_document_response.name
print(create_document_response)

# Chunks - add another webpage from Google for Developers
with(urlopen("https://developers.googleblog.com/2023/12/how-its-made-gemini-multimodal-prompting.html")) as f:
  html = f.read().decode("utf-8")

# Chunk the file using HtmlChunker
chunker = HtmlChunker(
    max_words_per_aggregate_passage=100,
    greedily_aggregate_sibling_nodes=False,
)
passages = chunker.chunk(html)

# Create `Chunk` entities.
chunks = []
for passage in passages:
    chunk = glm.Chunk(data={'string_value': passage})
    chunk.custom_metadata.append(glm.CustomMetadata(key="tags",
                                                    string_list_value=glm.StringList(
                                                        values=["Google For Developers", "Gemini API", "Blog", "Announcement"])))
    chunk.custom_metadata.append(glm.CustomMetadata(key="chunking_strategy",
                                                    string_value="no_aggregate_sibling_nodes"))
    chunk.custom_metadata.append(glm.CustomMetadata(key = "publish_date",
                                                    numeric_value = 20231206))
    chunks.append(chunk)

# Make the request
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

查询语料库

使用 QueryCorpusRequest 方法执行语义搜索以获取相关的段落。

  • results_count:指定要返回的段落数量。最大值为 100。如果未指定,API 最多返回 10 个 Chunk
  • metadata_filters:按 chunk_metadatadocument_metadata 过滤。每个 MetadataFilter 都需要对应一个唯一的键。多个 MetadataFilter 对象通过逻辑 AND 连接。类似元数据过滤条件由逻辑 OR 联接。一些示例:
(year >= 2020 OR year < 2010) AND (genre = drama OR genre = action)

metadata_filter = [
  {
    key = "document.custom_metadata.year"
    conditions = [
      {int_value = 2020, operation = GREATER_EQUAL},
      {int_value = 2010, operation = LESS}]
  },
  {
    key = "document.custom_metadata.genre"
    conditions = [
      {string_value = "drama", operation = EQUAL},
      {string_value = "action", operation = EQUAL} }]
  }]

请注意,只有数值支持对同一个键使用“AND”。字符串 值仅支持对同一个键使用“OR”。

("Google for Developers" in tags) and (20230314 > publish_date)

metadata_filter = [
 {
    key = "chunk.custom_metadata.tags"
    conditions = [
    {string_value = 'Google for Developers', operation = INCLUDES},
  },
  {
    key = "chunk.custom_metadata.publish_date"
    conditions = [
    {numeric_value = 20230314, operation = GREATER_EQUAL}]
  }]
user_query = "What is the purpose of Project IDX?"
results_count = 5

# Add metadata filters for both chunk and document.
chunk_metadata_filter = glm.MetadataFilter(key='chunk.custom_metadata.tags',
                                           conditions=[glm.Condition(
                                              string_value='Google For Developers',
                                              operation=glm.Condition.Operator.INCLUDES)])

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
request = glm.QueryCorpusRequest(name=corpus_resource_name,
                                 query=user_query,
                                 results_count=results_count,
                                 metadata_filters=[chunk_metadata_filter])
query_corpus_response = retriever_service_client.query_corpus(request)
print(query_corpus_response)

归因问答

使用 GenerateAnswer 方法可对您的文档、语料库或一组段落执行归因问答。

归因问答 (AQA) 是指根据给定的情境回答问题并提供出处,同时最大限度地减少幻觉。

在需要 AQA 的情况下,与使用未调整的 LLM 相比,GenerateAnswer 具有多项优势:

  • 底层模型已经过训练,能够仅返回基于所提供上下文的答案。
  • 它可识别提供方说明(对答案有帮助的所提供上下文的细分)。借助提供方说明,用户可以验证答案。
  • 它会估算给定对(问题、上下文)对的 answerable_probability,这进一步使您能够根据所返回的答案有实际意义且正确的可能性来改变产品行为。

answerable_probability和“我不知道”问题

在某些情况下,对这个问题的最佳回答是“我不知道”。例如,如果所提供的上下文不包含问题的答案,相应问题会被视为“无法解答”。

AQA 模型非常擅长识别此类情况。它甚至可以区分是否回答和不回答的程度。

不过,GenerateAnswer API 通过以下方式将最终决定权交到您手中:

  • 始终尝试返回有依据的答案 - 即使相应答案不太可能有依据且正确。
  • 返回值 answerable_probability - 模型对答案有基础且正确的可能性的估算值。

answerable_probability偏低可能是由于以下一个或多个因素造成的:

  • 模型不确定其答案是否正确。
  • 模型不确定其回答是否基于引用的段落;答案可能是根据世界知识得出的。例如:question="1+1=?", passages=["2+2=4”]answer=2, answerable_probability=0.02
  • 模型提供的相关信息并未完全回答问题。示例:question="Is it available in my size?, passages=["Available in sizes 5-11"]answer="Yes it is available in sizes 5-11", answerable_probability=0.03"
  • GenerateAnswerRequest 中未提出格式正确的问题。

由于较低的 answerable_probability 表示 GenerateAnswerResponse.answer 可能有误或缺乏依据,因此强烈建议您通过检查 answerable_probability 进一步处理响应

answerable_probability 较低时,某些客户端可能希望:

  • 显示消息,表示“无法回答该问题”最终用户
  • 回退到通用 LLM,根据世界知识回答此问题。此类回退的阈值和性质取决于具体用例。answerable_probability <= 0.5 是一个不错的起始阈值。

AQA 实用提示

如需查看完整的 API 规范,请参阅 GenerateAnswerRequest API 参考文档

  • 段落长度:建议每个段落最多 300 个词元。
  • 段落排序: <ph type="x-smartling-placeholder">
  • 限制:AQA 模型专门用于问答。对于创意撰写、摘要等其他用例,请通过 GenerateContent 调用通用模型。
    • 聊天:如果已知用户输入内容是在特定上下文中可以回答的问题,则 AQA 可以回答聊天询问。但是,如果用户输入可以是任何类型的输入,那么通用模型可能是更好的选择。
  • 温度: <ph type="x-smartling-placeholder">
      </ph>
    • 通常,为了获得准确的 AQA 结果,建议采用相对较低 (~0.2) 的温度。
    • 如果您的用例依赖于确定性输出,则将 temperature 设置为 0。
user_query = "What is the purpose of Project IDX?"
answer_style = "ABSTRACTIVE" # Or VERBOSE, EXTRACTIVE
MODEL_NAME = "models/aqa"

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
content = glm.Content(parts=[glm.Part(text=user_query)])
retriever_config = glm.SemanticRetrieverConfig(source=corpus_resource_name, query=content)
req = glm.GenerateAnswerRequest(model=MODEL_NAME,
                                contents=[content],
                                semantic_retriever=retriever_config,
                                answer_style=answer_style)
aqa_response = generative_service_client.generate_answer(req)
print(aqa_response)
# Get the metadata from the first attributed passages for the source
chunk_resource_name = aqa_response.answer.grounding_attributions[0].source_id.semantic_retriever_chunk.chunk
get_chunk_response = retriever_service_client.get_chunk(name=chunk_resource_name)
print(get_chunk_response)

更多选项:使用内嵌段落的 AQA

或者,您也可以直接使用 AQA 端点,而不通过传递 inline_passages 来使用 Semantic Retriever API。

user_query = "What is AQA from Google?"
user_query_content = glm.Content(parts=[glm.Part(text=user_query)])
answer_style = "VERBOSE" # or ABSTRACTIVE, EXTRACTIVE
MODEL_NAME = "models/aqa"

# Create the grounding inline passages
grounding_passages = glm.GroundingPassages()
passage_a = glm.Content(parts=[glm.Part(text="Attributed Question and Answering (AQA) refers to answering questions grounded to a given corpus and providing citation")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_a, id="001"))
passage_b = glm.Content(parts=[glm.Part(text="An LLM is not designed to generate content grounded in a set of passages. Although instructing an LLM to answer questions only based on a set of passages reduces hallucination, hallucination still often occurs when LLMs generate responses unsupported by facts provided by passages")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_b, id="002"))
passage_c = glm.Content(parts=[glm.Part(text="Hallucination is one of the biggest problems in Large Language Models (LLM) development. Large Language Models (LLMs) could produce responses that are fictitious and incorrect, which significantly impacts the usefulness and trustworthiness of applications built with language models.")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_c, id="003"))

# Create the request
req = glm.GenerateAnswerRequest(model=MODEL_NAME,
                                contents=[user_query_content],
                                inline_passages=grounding_passages,
                                answer_style=answer_style)
aqa_response = generative_service_client.generate_answer(req)
print(aqa_response)

共享语料库

您可以选择使用 CreatePermissionRequest API 与他人分享语料库。

限制:

  • 有两种共享角色:READEREDITOR
    • READER 可以查询语料库。
    • WRITER 拥有读取者权限,还可以修改和共享语料库。
  • 通过向 EVERYONE 授予 user_type 读取权限,可将语料库公开。
# Replace your-email@gmail.com with the email added as a test user in the OAuth Quickstart
shared_user_email = "TODO-your-email@gmail.com" #  @param {type:"string"}
user_type = "USER"
role = "READER"

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
request = glm.CreatePermissionRequest(
    parent=corpus_resource_name,
    permission=glm.Permission(grantee_type=user_type,
                              email_address=shared_user_email,
                              role=role))
create_permission_response = permission_service_client.create_permission(request)
print(create_permission_response)

删除语料库

使用 DeleteCorpusRequest 可删除用户语料库以及所有关联的 DocumentChunk

请注意,非空语料库会在未指定 force=True 标志的情况下抛出错误。如果您设置了 force=True,则与此 Document 相关的所有 Chunk 和对象也将被删除。

如果 force=False(默认值)且 Document 包含任何 Chunk,则返回 FAILED_PRECONDITION 错误。

# Set force to False if you don't want to delete non-empty corpora.
req = glm.DeleteCorpusRequest(name=corpus_resource_name, force=True)
delete_corpus_response = retriever_service_client.delete_corpus(req)
print("Successfully deleted corpus: " + corpus_resource_name)

总结和补充阅读材料

本指南介绍了语义检索器和归因问题解答 (AQA) API 的相关问题,并展示了如何使用它对自定义文本数据执行语义信息检索。请注意,此 API 也可与 LlamaIndex 数据框架搭配使用。如需了解详情,请参阅教程

另请参阅 API 文档,详细了解其他可用功能。

附录:使用用户凭据设置 OAuth

请按照 OAuth 快速入门中的步骤来设置 OAuth 身份验证。

  1. 配置 OAuth 同意屏幕

  2. 为桌面应用授权凭据。如需在 Colab 中运行此笔记本,请先将您的凭据文件(通常为 client_secret_*.json)重命名为 client_secret.json。然后使用左侧边栏中的文件图标和“上传”图标上传文件,如以下屏幕截图所示。

# Replace TODO-your-project-name with the project used in the OAuth Quickstart
project_name = "TODO-your-project-name" #  @param {type:"string"}
# Replace TODO-your-email@gmail.com with the email added as a test user in the OAuth Quickstart
email = "TODO-your-email@gmail.com" #  @param {type:"string"}
# Rename the uploaded file to `client_secret.json` OR
# Change the variable `client_file_name` in the code below.
client_file_name = "client_secret.json"

# IMPORTANT: Follow the instructions from the output - you must copy the command
# to your terminal and copy the output after authentication back here.
!gcloud config set project $project_name
!gcloud config set account $email

# NOTE: The simplified project setup in this tutorial triggers a "Google hasn't verified this app." dialog.
# This is normal, click "Advanced" -> "Go to [app name] (unsafe)"
!gcloud auth application-default login --no-browser --client-id-file=$client_file_name --scopes="https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform"

初始化客户端库,然后从创建语料库开始重新运行笔记本。

import google.ai.generativelanguage as glm

generative_service_client = glm.GenerativeServiceClient()
retriever_service_client = glm.RetrieverServiceClient()
permission_service_client = glm.PermissionServiceClient()