Skip to content

Commit c9c6b83

Browse files
committed
Add more to the prompt to have it know about other methods
1 parent 981b158 commit c9c6b83

File tree

5 files changed

+268
-13
lines changed

5 files changed

+268
-13
lines changed

shiny/assistant/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
chatlas/
22
prompt.xml
33
rsconnect-python/
4+
_swagger.json
5+
_swagger_prompt.md
6+
repomix-instruction.md

shiny/assistant/_update_prompt.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
def cleanup() -> None:
1818
# Clean slate
1919
print("Clean up")
20-
for f in ["typings", "repomix-output.txt", "repomix-output.xml"]:
20+
for f in [
21+
"typings",
22+
"repomix-output.txt",
23+
"repomix-output.xml",
24+
"repomix-instruction.md",
25+
]:
2126
path = repo_root / f
2227
if os.path.exists(path):
2328
print("Removing path:", path.relative_to(repo_root))
@@ -43,8 +48,20 @@ async def main() -> None:
4348
)
4449
print("--\n")
4550

46-
print("Creating repomix output")
51+
print("Getting Swagger information")
52+
os.system("python shiny/assistant/_update_swagger.py")
53+
54+
with open(repo_root / "shiny" / "assistant" / "repomix-instruction.md", "w") as prompt_f:
55+
with open(repo_root / "shiny" / "assistant" / "instructions.md", "r") as instructions_f:
56+
prompt_f.write(instructions_f.read())
57+
58+
prompt_f.write("\n")
4759

60+
with open(repo_root / "shiny" / "assistant" / "_swagger_prompt.md", "r") as swagger_f:
61+
prompt_f.write(swagger_f.read())
62+
print("--\n")
63+
64+
print("Creating repomix output")
4865
# Assert npx exists in system
4966
assert os.system("npx --version") == 0, "npx not found in system. Please install Node.js"
5067
os.system(

shiny/assistant/_update_swagger.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
from __future__ import annotations
2+
3+
from copy import deepcopy
4+
from pathlib import Path
5+
6+
from typing_extensions import Any, NotRequired, TypedDict, TypeVar
7+
8+
here = Path(__file__).parent
9+
T = TypeVar("T")
10+
11+
12+
def find_value(root, path):
13+
"""
14+
Find a value in an object graph.
15+
16+
This function is used to follow the specified path through the object graph at root
17+
and return the item in the graph, if any, that the path refers to.
18+
19+
:param root: the root of the object graph to traverse.
20+
:param path: the path through the graph to take.
21+
:return: the resulting value or None.
22+
"""
23+
if isinstance(path, str):
24+
path = path.split("/")
25+
parent = root
26+
for part in path:
27+
if part in parent:
28+
parent = parent[part]
29+
else:
30+
return None
31+
return parent
32+
33+
34+
def expand_refs(
35+
document, obj
36+
) -> Any: # Use `Any` for return type to hack around typing requirement
37+
"""
38+
Expands `ref`s in the given object.
39+
40+
Returns an object semantically equivalent to the original but with references expanded.
41+
42+
Parameters
43+
----------
44+
document
45+
the master swagger document containing the responses and definitions.
46+
obj
47+
is either a normal swagger object, a ref object, or a swagger object with a schema.
48+
"""
49+
if isinstance(obj, list):
50+
return [expand_refs(document, item) for item in obj]
51+
elif isinstance(obj, dict):
52+
if "$ref" in obj:
53+
ref_path = obj["$ref"].strip("#/").split("/")
54+
ref_value = find_value(document, ref_path)
55+
if ref_value is None:
56+
raise RuntimeError(f"Reference {obj['$ref']} not found in the document.")
57+
return expand_refs(document, ref_value)
58+
else:
59+
return {key: expand_refs(document, value) for key, value in obj.items()}
60+
else:
61+
return obj
62+
63+
64+
class SwaggerOperation(TypedDict, total=False):
65+
operationId: str
66+
tags: list[str]
67+
summary: str
68+
description: str
69+
parameters: list[dict[str, Any]]
70+
responses: dict[str, Any]
71+
72+
73+
class SwaggerDocument(TypedDict):
74+
paths: NotRequired[dict[str, SwaggerOperation]]
75+
parameters: NotRequired[dict[str, Any]]
76+
responses: NotRequired[dict[str, Any]]
77+
definitions: NotRequired[dict[str, Any]]
78+
79+
80+
def expand_all_references(document: SwaggerDocument) -> SwaggerDocument:
81+
"""
82+
Expands all JSON references.
83+
84+
Expands all references ($ref) in the merged swagger document by replacing them with
85+
their full definitions.
86+
87+
This returns a new document with all references expanded.
88+
89+
Arguments
90+
---------
91+
document
92+
The dictionary representing the Swagger document to process
93+
94+
Returns
95+
-------
96+
:
97+
The processed Swagger document with all references expanded.
98+
"""
99+
ret_document = deepcopy(document)
100+
# List of error response keys to ignore
101+
error_responses = [
102+
"BadRequest",
103+
"Unauthorized",
104+
"PaymentRequired",
105+
"Forbidden",
106+
"NotFound",
107+
"Conflict",
108+
"APIError",
109+
"InternalServerError",
110+
]
111+
112+
# We need to expand refs in paths
113+
if "paths" in ret_document:
114+
for _path, operations in ret_document["paths"].items():
115+
for _method, operation in operations.items():
116+
if not isinstance(operation, dict):
117+
continue
118+
# Expand refs in parameters
119+
if "parameters" in operation:
120+
operation["parameters"] = expand_refs(ret_document, operation["parameters"])
121+
122+
# Expand refs in responses
123+
if "responses" in operation:
124+
for code, response in operation["responses"].items():
125+
if "schema" in response and code not in error_responses:
126+
response["schema"] = expand_refs(ret_document, response["schema"])
127+
128+
# Expand refs in top-level parameters
129+
if "parameters" in ret_document:
130+
ret_document["parameters"] = expand_refs(ret_document, ret_document["parameters"])
131+
132+
# Expand refs in top-level responses, ignoring error responses
133+
if "responses" in ret_document:
134+
for response_key, response_value in ret_document["responses"].items():
135+
if response_key not in error_responses:
136+
ret_document["responses"][response_key] = expand_refs(ret_document, response_value)
137+
138+
# Expand refs in definitions
139+
if "definitions" in ret_document:
140+
ret_document["definitions"] = expand_refs(ret_document, ret_document["definitions"])
141+
142+
return ret_document
143+
144+
145+
def require_swagger():
146+
if not (here / "swagger.json").exists():
147+
import urllib.request
148+
149+
urllib.request.urlretrieve(
150+
"https://docs.posit.co/connect/api/swagger.json",
151+
here / "_swagger.json",
152+
)
153+
154+
import json
155+
156+
with open(here / "_swagger.json") as f:
157+
doc = json.load(f)
158+
159+
swagger = expand_all_references(doc)
160+
return swagger
161+
162+
163+
class OperationDef(TypedDict):
164+
name: str
165+
tags: list[str]
166+
method: str
167+
route: str
168+
definition: dict[str, Any]
169+
170+
171+
def transform_swagger_to_operation_dict(swagger_dict: SwaggerDocument) -> dict[str, OperationDef]:
172+
"""
173+
Swagger to operation dictionary transformation.
174+
175+
Transforms the structure of a Swagger dictionary to create a dictionary where each entry key is
176+
the operation ID and the value is the definition for that operation, including the HTTP verb
177+
and the route.
178+
179+
Args:
180+
swagger_dict: The dictionary representing the Swagger document.
181+
182+
Returns
183+
-------
184+
:
185+
A dictionary where each key is an operation ID and the value is the operation definition.
186+
"""
187+
operation_dict = {}
188+
189+
if "paths" in swagger_dict:
190+
for route, operations in swagger_dict["paths"].items():
191+
for method, operation in operations.items():
192+
if not isinstance(operation, dict):
193+
continue
194+
if "operationId" in operation:
195+
operation_id = operation["operationId"]
196+
tags = operation["tags"] if "tags" in operation else []
197+
# response_parent = operation["responses"]["200"] or operation["responses"]["204"]
198+
# if response_parent and "response"
199+
operation_dict[operation_id] = {
200+
"name": operation_id,
201+
"tags": tags,
202+
"method": method,
203+
"route": route,
204+
"definition": operation,
205+
}
206+
207+
return operation_dict
208+
209+
210+
def main():
211+
swagger = require_swagger()
212+
213+
operations = transform_swagger_to_operation_dict(require_swagger())
214+
215+
with open(here / "_swagger_prompt.md", "w") as f:
216+
f.write(
217+
"If an answer can not be resolved, suggest to the user that they can explore calling these API routes themselves. Never produce code that calls these routes as we do not know the return type or successful status codes.\n\nAPI Routes:\n"
218+
""
219+
)
220+
221+
for operation in operations.values():
222+
# print("Operation:", operation["name"])
223+
224+
# "GET /v1/tasks/{id} Get task details"
225+
f.write(
226+
"* "
227+
+ operation["method"].upper()
228+
+ " "
229+
+ operation["route"]
230+
+ " "
231+
+ operation["definition"]["summary"].replace("\n", " ").strip()
232+
+ "\n",
233+
)
234+
235+
236+
if __name__ == "__main__":
237+
main()

shiny/assistant/instructions.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
You are an assistant that can create Posit SDK python code that can provide code solutions to interact with the user's local Posit Connect instance.
2+
3+
All of your answers need to be code based. When returning answers, please restate the question and then provide the code within a code block.
4+
5+
This is a serious exercise. Please provide evidence for each answer and double check the answers for accuracy. If a question cannot be answered using the materials and tools provided, please explicitly say so.
6+
7+
If a question is unclear, please ask for clarification.
8+
9+
If you feel there is an opportunity for further exploration, please suggest the prompts. Wrap each suggested prompt within a <a class="sdk_suggested_prompt"></a> tag.

shiny/assistant/repomix-instruction.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)