Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add sample agent #6

Merged
merged 19 commits into from
May 24, 2024
17 changes: 16 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ jobs:
run: poetry install --no-interaction --no-ansi
- name: Lint
run: poetry run flake8
tests:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: poetry
- name: Install Packages
run: poetry install --no-interaction --no-ansi
- name: Run tests
run: poetry run pytest -v -s -W ignore::DeprecationWarning
check-version:
name: "Check version"
runs-on: ubuntu-latest
Expand All @@ -63,7 +78,7 @@ jobs:
package_manager: "poetry"
publish:
name: "Publish package"
needs: [preconditions, lint, dependency-check, tests, check-version]
needs: [preconditions, lint, tests, check-version]
runs-on: ubuntu-latest
# comment the line above to force publish
if: ${{ needs.check-version.outputs.is_new_version == 'true' }}
Expand Down
114 changes: 113 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,113 @@
# main
# nice-fetch-ai-adapter

The purpose of this repository is to allow interaction between nice-agent-portal (PEER API), Veritable Peer and Cambridge's 'intelligent agent'.

### Envars

Can be found in `core>config.py`.
|ENV|required|default|
|--|------|----------|
|VERITABLE_URL|Y|http://localhost:3010|
|PEER_URL|Y| http://localhost:3001/api|
|AGENT_ADDRESS|Y|agent1qt8q20p0vsp7y5dkuehwkpmsppvynxv8esg255fwz6el68rftvt5klpyeuj|

### Endpoints

The endpoints in this repository are:
| endpoint |HTTP Methods| usage |
|----------|----|----------|
| "/send-query" |POST| Accepts query from Peer API, forwards it to Sample Agent & then to Veritable Peer. |
| "/webhooks/drpc"|POST| Accepts posts from veritable Cloudagent and passes them to PeerApi. These are either queries for peerApi or query responses for peer API. |
| "/receive-response" |POST| This endpoint receives information from chainvine and passes it to Veritable as a response. |

### Payload schemas:

#### POST/send-query schema inbound:

-> we only support method `query` for now

```
{
"jsonrpc": "2.0",
"method": "query",
"params": [{<query json object>}],
"id": "string"
}
```

#### POST/send-query schema outbound:

-> status 202 request received

#### POST/webhooks/drpc schema inbound:

- This schema can only include either `response` or `request`.
- If `request` is present, `role` must be `server` and if `response` is present `role` must be `client`

```
{
"createdAt": "2024-05-23T08:23:49.183Z",
"request": {
"jsonrpc": "2.0",
"method": "query",
"params": [{<query json object>}],
"id": "string"
},
"response": {
"jsonrpc": "2.0",
"result": {<response json object>},
"error": {
"code": -32601,
"message": "string",
"data": "string"
},
"id": <must match request.id>
},
"connectionId": "string",
"role": "client" OR "server",
"state": "request-sent",
"threadId": "string",
"id": "string"
}
```

#### POST/webhooks/drpc schema outbound:

-> status 202 request received

#### POST/receive-response schema inbound:

```
{
jsonrpc: '2.0',
result: {<response json object>},
id: <must match request.id>
"error": {
"code": -32601,
"message": "string",
"data": "string"
},
}

```

#### POST/receive-response schema outbound:

-> status 202 request received

Ellenn-A marked this conversation as resolved.
Show resolved Hide resolved
### Prerequisites:

- python 3.12 or higher
- poetry installed

### To start the repo

Run `poetry install`

Some endpoints require interaction with fetch.ai agent. We have included such `SampleAgent` in this repo.
In order to bring up the repo run `python run.py` from your terminal (in root directory). This will bring up both the Sample fetch.ai agent and our app with swagger interface on `http://0.0.0.0:8000/docs`.
On startup agent prints it's own address -> please include this address in `core/config.py`(if it is different to the one hardcoded there).

### To run tests:

`poetry run pytest -s`
4 changes: 4 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
class AppSettings(BaseSettings):
VERITABLE_URL: str = os.getenv("VERITABLE_URL", "http://localhost:3010")
PEER_URL: str = os.getenv("PEER_URL", "http://localhost:3001/api")
AGENT_ADDRESS: str = os.getenv(
"AGENT_ADDRESS",
"agent1qt8q20p0vsp7y5dkuehwkpmsppvynxv8esg255fwz6el68rftvt5klpyeuj",
)


settings = AppSettings()
156 changes: 120 additions & 36 deletions app/routes/posts.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import json
from datetime import datetime
from enum import IntEnum, StrEnum
from typing import Any, List, Optional
from typing import Any, List, Optional, Union

import httpx
from core.config import settings
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from uagents import Model
from uagents.query import query

veritableUrl = settings.VERITABLE_URL
peerUrl = settings.PEER_URL


class Query(BaseModel):
message: dict
AGENT_ADDRESS = settings.AGENT_ADDRESS


class DrpcRequestObject(Model):
jsonrpc: str
method: str
params: Optional[List | object]
id: Optional[str | int]
id: str | int


class DrpcErrorCode(IntEnum):
Expand All @@ -43,7 +41,7 @@ class DrpcResponseObject(Model):
jsonrpc: str
result: Optional[Any]
error: Optional[DrpcResponseError]
id: Optional[str | int]
id: Union[str | int]


class DrpcRole(StrEnum):
Expand All @@ -57,13 +55,6 @@ class DrpcState(StrEnum):
Completed = ("completed",)


# RPC Response example
# --> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
# <-- {"jsonrpc": "2.0", "result": -19, "id": 2}
class Response(Model):
message: dict


class DrpcEvent(Model):
createdAt: datetime
request: Optional[DrpcRequestObject | List[DrpcRequestObject]]
Expand All @@ -76,18 +67,29 @@ class DrpcEvent(Model):
_tags: dict


class AgentRequest(Model):
params: List[str]
id: str


router = APIRouter()


async def postToVeritable(req: Query) -> JSONResponse:
async def agent_query(req: AgentRequest):
response = await query(destination=AGENT_ADDRESS, message=req, timeout=15.0)
data = json.loads(response.decode_payload())
return [data]


async def postToVeritable(req: DrpcRequestObject) -> JSONResponse:
async with httpx.AsyncClient() as client:
response = await client.post(f"{veritableUrl}/drcp/request", json=req)
response = await client.post(f"{veritableUrl}/drpc/request", json=req)
return [response.status, response.json()]


async def postResponseToVeritable(req: Response) -> JSONResponse:
async def postResponseToVeritable(req: DrpcResponseObject) -> JSONResponse:
async with httpx.AsyncClient() as client:
response = client.post(f"{veritableUrl}/drcp/response", json=req)
response = await client.post(f"{veritableUrl}/drpc/response", json=req)
return [response.status, response.json()]


Expand All @@ -103,20 +105,66 @@ async def peerReceivesQuery(req: DrpcEvent) -> JSONResponse:
return [response.status, response.json()]


async def create_error_response(
id: Union[str, int],
error_code: DrpcErrorCode,
error_message: str,
error_data: Optional[Any] = None,
) -> DrpcResponseObject:
return DrpcResponseObject(
jsonrpc="2.0",
id=id,
error=DrpcResponseError(
code=error_code, message=error_message, data=error_data
),
result=None,
)


# Query from PeerApi to query agent & veritable
@router.post("/send-query", name="test-name", status_code=202)
async def send_query(req: Query): # need to define Query
async def send_query(req: DrpcRequestObject):
try:
response = await postToVeritable(req)
if req.method != "query":
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.invalid_request,
error_message="Only supported method is query",
)
)
agentRequest = AgentRequest(params=req.params, id=str(req.id))
agentQueryResp = await agent_query(agentRequest)
expected_response = [
{"text": "Successful query response from the Sample Agent"}
]
if agentQueryResp != expected_response:
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.server_error,
error_message=f"Query Agent returned unexpected response. Response returned: {agentQueryResp}",
)
)
# do sth based on the agentQueryResponse??
req_dict = dict(req)
response = await postToVeritable(req_dict)
if response[0] != "202":
raise ValueError("Response status is not 202")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.server_error,
error_message="Response status is not 202",
)
)
return response
except Exception as e:

raise HTTPException(status_code=500, detail=str(e))


@router.post(
"/webhooks/drpc", name="webhooks-drpc", status_code=200
) # from veritable cloudagent to peerAPI
# from veritable cloudagent to peerAPI
@router.post("/webhooks/drpc", name="webhooks-drpc", status_code=200)
async def drpc_event_handler(req: DrpcEvent):
try:
req_dict = dict(req)
Expand All @@ -135,32 +183,68 @@ async def drpc_event_handler(req: DrpcEvent):
response_check = req_dict["response"]
role_check = req_dict["role"]
if request_check and response_check:
raise ValueError("JSON body cannot contain both 'request' and 'response'")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.invalid_request,
error_message="JSON body cannot contain both 'request' and 'response'",
)
)
if request_check and role_check != "server":
raise ValueError("If 'request' is present, 'role' must be 'server'")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.invalid_request,
error_message="If 'request' is present, 'role' must be 'server'",
)
)
if response_check and role_check != "client":
raise ValueError("If 'response' is present, 'role' must be 'client'")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.invalid_request,
error_message="If 'response' is present, 'role' must be 'client'",
)
)
if role_check == "client":
response = await peerReceivesResponse(req_dict)
elif role_check == "server":
response = await peerReceivesQuery(req_dict)
else:
raise ValueError("Error in request body.")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.invalid_request,
error_message="Error in request body.",
)
)
if response[0] != "200":
raise ValueError("Response status is not 200")
raise ValueError(
await create_error_response(
id=req.id,
error_code=DrpcErrorCode.server_error,
error_message=f"Response status from Peer Api is not 200. Response status:{response[0]}, body: {response[1]}",
)
)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Ellenn-A marked this conversation as resolved.
Show resolved Hide resolved


@router.post(
"/receive-response", name="receive-response", status_code=200
) # this receives response from chainvine and it forwards info to veritable
async def receive_response(resp: Response): # basic RPC response
# this receives response from chainvine and it forwards info to veritable
@router.post("/receive-response", name="receive-response", status_code=200)
async def receive_response(resp: DrpcResponseObject):
try:
response = await postResponseToVeritable(resp)
resp_dict = dict(resp)
response = await postResponseToVeritable(resp_dict)
if response[0] != "200":
raise ValueError("Response status is not 200")
raise ValueError(
await create_error_response(
id=resp.id,
error_code=DrpcErrorCode.server_error,
error_message=f"Response status from Peer Api is not 200. Response status:{response[0]}, body: {response[1]}",
)
)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Loading