Skip to content

Commit

Permalink
feat: Add PlaygroundsSubgraphInspectorToolSpec to llama_hub (run-llam…
Browse files Browse the repository at this point in the history
…a#535)

feat: Add PlaygroundsSubgraphInspectorToolSpec to llama_hub
  • Loading branch information
Tachikoma000 authored Sep 25, 2023
1 parent 56d29d9 commit a6188c8
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 0 deletions.
5 changes: 5 additions & 0 deletions llama_hub/tools/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
"author": "tachi",
"keywords": ["subgraph", "blockchain", "playgroundsapi", "graphql", "decentralized", "thegraph"]
},
"PlaygroundsSubgraphInspectorToolSpec": {
"id": "tools/playgrounds_subgraph_inspector",
"author": "tachi",
"keywords": ["subgraph", "blockchain", "playgroundsapi", "graphql", "decentralized", "thegraph"]
},
"PythonFileToolSpec": {
"id": "tools/python_file",
"author": "ajhofmann"
Expand Down
76 changes: 76 additions & 0 deletions llama_hub/tools/playgrounds_subgraph_inspector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# playgrounds_subgraph_inspector

Playgrounds API is a service provided by [Playgrounds Analytics](https://playgrounds.network) to facilitate interactions with decentralized subgraphs (indexed blockchain datasets).

The `PlaygroundsSubgraphInspectorToolSpec` is a tool designed for LLM agents to introspect and understand the schema of subgraphs on The Graph's decentralized network via the Playgrounds API.

This tool is specifically designed to be used alongside [Llama index](https://github.com/jerryjliu/llama_index) or [langchain](https://python.langchain.com/docs/modules/agents/tools/custom_tools).

- To learn more about Playgrounds API, please visit our website: [Playgrounds Network](https://playgrounds.network/)
- Obtain your Playgrounds API Key and get started for free [here](https://app.playgrounds.network/signup).
- Discover any Subgraph (dataset) you need [here](https://thegraph.com/explorer).

## Advantages of this tool:

- **Introspection of Decentralized Subgraphs (Datasets)**: Understand the schema of any subgraph without hassle.
- **LLM x Blockchain Data**: Develop AI applications that leverage introspective insights from blockchain data.

## Basic Usage:

To utilize the tool, initialize it with the appropriate `identifier` (Subgraph ID or Deployment ID), `api_key`, and specify if you're using a deployment ID.

```python
import openai
from llama_index.agent import OpenAIAgent
from llama_hub.tools.playgrounds_subgraph_inspector.base import PlaygroundsSubgraphInspectorToolSpec

def inspect_subgraph(
openai_api_key: str,
playgrounds_api_key: str,
identifier: str,
use_deployment_id: bool,
user_prompt: str
):
"""
Introspect a subgraph using OpenAIAgent and Playgrounds API with the provided parameters.
Args:
openai_api_key (str): API key for OpenAI.
playgrounds_api_key (str): API key for Playgrounds.
identifier (str): Identifier for the subgraph or deployment.
use_deployment_id (bool): If True, uses deployment ID in the URL.
user_prompt (str): User's question or prompt for the agent.
Returns:
str: Agent's response.
"""
# Set the OpenAI API key
openai.api_key = openai_api_key

# Initialize the inspector with the provided parameters
inspector_spec = PlaygroundsSubgraphInspectorToolSpec(
identifier=identifier,
api_key=playgrounds_api_key,
use_deployment_id=use_deployment_id
)

# Integrate the tool with the agent
agent = OpenAIAgent.from_tools(inspector_spec.to_tool_list())

# Send the user prompt to the agent
response = agent.chat(user_prompt)
return response


if __name__ == "__main__":
query = inspect_subgraph(
openai_api_key='YOUR_OPENAI_API_KEY',
playgrounds_api_key="YOUR_PLAYGROUNDS_API_KEY",
identifier="YOUR_SUBGRAPH_OR_DEPLOYMENT_IDENTIFIER",
use_deployment_id=False,
user_prompt='Which entities will help me understand the usage of Uniswap V3?'
)
```
Visit here for more in-depth [Examples](https://github.com/Tachikoma000/playgrounds_subgraph_connector/blob/main/introspector_agent_tool/examples.ipynb).

This inspector is designed to be used as a way to understand the schema of subgraphs and subgraph data being loaded into [LlamaIndex](https://github.com/jerryjliu/gpt_index/tree/main/gpt_index) and/or subsequently used as a Tool in a [LangChain](https://github.com/hwchase17/langchain) Agent.
Empty file.
251 changes: 251 additions & 0 deletions llama_hub/tools/playgrounds_subgraph_inspector/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
"""PlaygroundsSubgraphInspectorToolSpec."""

from typing import Optional, Union
import requests
from llama_hub.tools.graphql.base import GraphQLToolSpec

class PlaygroundsSubgraphInspectorToolSpec(GraphQLToolSpec):
"""
Connects to subgraphs on The Graph's decentralized network via the Playgrounds API and introspects the subgraph.
Provides functionalities to process and summarize the introspected schema for easy comprehension.
Attributes:
spec_functions (list): List of functions that specify the tool's capabilities.
url (str): The endpoint URL for the GraphQL requests.
headers (dict): Headers used for the GraphQL requests.
"""
spec_functions = ["introspect_and_summarize_subgraph"]

def __init__(self, identifier: str, api_key: str, use_deployment_id: bool = False):
"""
Initialize the connection to the specified subgraph on The Graph's network.
Args:
identifier (str): The subgraph's identifier or deployment ID.
api_key (str): API key for the Playgrounds API.
use_deployment_id (bool): If True, treats the identifier as a deployment ID. Default is False.
"""
self.url = self._generate_url(identifier, use_deployment_id)
self.headers = {
"Content-Type": "application/json",
"Playgrounds-Api-Key": api_key
}

def _generate_url(self, identifier: str, use_deployment_id: bool) -> str:
"""
Generate the appropriate URL based on the identifier and whether it's a deployment ID or not.
Args:
identifier (str): The subgraph's identifier or deployment ID.
use_deployment_id (bool): If True, constructs the URL using the deployment ID.
Returns:
str: The constructed URL.
"""

endpoint = "deployments" if use_deployment_id else "subgraphs"
return f"https://api.playgrounds.network/v1/proxy/{endpoint}/id/{identifier}"

def introspect_and_summarize_subgraph(self) -> str:
"""
Introspects the subgraph and summarizes its schema into textual categories.
Returns:
str: A textual summary of the introspected subgraph schema.
"""
introspection_query = """
query {
__schema {
types {
kind
name
description
enumValues {
name
}
fields {
name
args {
name
}
type {
kind
name
ofType {
name
}
}
}
}
}
}
"""
response = self._graphql_request(introspection_query)
if "data" in response:
result = response["data"]
processed_subgraph = self._process_subgraph(result)
return self.subgraph_to_text(processed_subgraph)
else:
return "Error during introspection."

def _graphql_request(self, query: str) -> dict:
"""
Execute a GraphQL query against the subgraph's endpoint.
Args:
query (str): The GraphQL query string.
Returns:
dict: Response from the GraphQL server, either containing the data or an error.
"""
payload = {'query': query.strip()}
try:
response = requests.post(self.url, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
return {"error": str(e)}

def _process_subgraph(self, result: dict) -> dict:
"""
Processes the introspected subgraph schema into categories based on naming conventions.
Args:
result (dict): Introspected schema result from the GraphQL query.
Returns:
dict: A processed representation of the introspected schema, categorized into specific entity queries, list entity queries, and other entities.
"""
processed_subgraph = {
"specific_entity_queries": {},
"list_entity_queries": {},
"other_entities": {}
}
for type_ in result['__schema']['types']:
if type_['name'].startswith('__'):
continue # Skip meta entities

entity_name = type_['name']
fields, args_required = self._get_fields(type_)
if fields:
# Determine category based on naming convention
if entity_name.endswith('s') and not args_required:
processed_subgraph["list_entity_queries"][entity_name] = fields
elif not entity_name.endswith('s') and args_required:
processed_subgraph["specific_entity_queries"][entity_name] = fields
else:
processed_subgraph["other_entities"][entity_name] = fields

return processed_subgraph

def _get_fields(self, type_):
"""
Extracts relevant fields and their details from a given type within the introspected schema.
Args:
type_ (dict): A type within the introspected schema.
Returns:
tuple: A tuple containing a list of relevant fields and a boolean indicating if arguments are required for the fields.
"""
fields = []
args_required = False
for f in (type_.get('fields') or []):
if f['name'] != '__typename' and not (f['name'].endswith('_filter') or f['name'].endswith('_orderBy') or f['name'].islower()):
field_info = {'name': f['name']}

# Check for enum values
if 'enumValues' in f['type'] and f['type']['enumValues']:
field_info['enumValues'] = [enum_val['name'] for enum_val in f['type']['enumValues']]

fields.append(field_info)
if f.get('args') and len(f['args']) > 0:
args_required = True
if f.get('type') and f['type'].get('fields'):
subfields, sub_args_required = self._get_fields(f['type'])
fields.extend(subfields)
if sub_args_required:
args_required = True
return fields, args_required

def format_section(self, category: str, description: str, example: str, entities: dict) -> str:
"""
Formats a given section of the subgraph introspection result into a readable string format.
Args:
category (str): The category name of the entities.
description (str): A description explaining the category.
example (str): A generic GraphQL query example related to the category.
entities (dict): Dictionary containing entities and their fields related to the category.
Returns:
str: A formatted string representation of the provided section data.
"""
section = [
f"Category: {category}",
f"Description: {description}",
"Generic Example:",
example,
"\nDetailed Breakdown:"
]

for entity, fields in entities.items():
section.append(f" Entity: {entity}")
for field_info in fields:
field_str = f" - {field_info['name']}"
if 'enumValues' in field_info:
field_str += f" (Enum values: {', '.join(field_info['enumValues'])})"
section.append(field_str)
section.append("") # Add a blank line for separation

section.append("") # Add another blank line for separation between sections
return "\n".join(section)

def subgraph_to_text(self, subgraph: dict) -> str:
"""
Converts a processed subgraph representation into a textual summary based on entity categories.
Args:
subgraph (dict): A processed representation of the introspected schema, categorized into specific entity queries, list entity queries, and other entities.
Returns:
str: A textual summary of the processed subgraph schema.
"""
sections = [
("Specific Entity Queries (Requires Arguments)",
"These queries target a singular entity and require specific arguments (like an ID) to fetch data.",
"""
{
entityName(id: "specific_id") {
fieldName1
fieldName2
...
}
}
""",
subgraph["specific_entity_queries"]),

("List Entity Queries (Optional Arguments)",
"These queries fetch a list of entities. They don't strictly require arguments but often accept optional parameters for filtering, sorting, and pagination.",
"""
{
entityNames(first: 10, orderBy: "someField", orderDirection: "asc") {
fieldName1
fieldName2
...
}
}
""",
subgraph["list_entity_queries"]),

("Other Entities",
"These are additional entities that may not fit the conventional singular/plural querying pattern of subgraphs.",
"",
subgraph["other_entities"])
]

result_lines = []
for category, desc, example, entities in sections:
result_lines.append(self.format_section(category, desc, example, entities))

return "\n".join(result_lines)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.31.0

0 comments on commit a6188c8

Please sign in to comment.