Skip to content

Commit d123ab6

Browse files
committed
Simple openapi documentation procudes an openapi yaml specification
1 parent 6a4d233 commit d123ab6

File tree

2 files changed

+116
-16
lines changed

2 files changed

+116
-16
lines changed

‎usecases/web_api_testing/prompt_engineer.py‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def in_context_learning(self):
100100
"""
101101
return str("\n".join(self._prompt_history[self.round]["content"] + [self.prompt]))
102102

103-
def chain_of_thought(self):
103+
def chain_of_thought(self,zero_shot = False):
104104
"""
105105
Generates a prompt using the chain-of-thought strategy. https://www.promptingguide.ai/techniques/cot
106106
@@ -109,10 +109,27 @@ def chain_of_thought(self):
109109
Returns:
110110
str: The generated prompt.
111111
"""
112+
112113
previous_prompt = self._prompt_history[self.round]["content"]
113-
chain_of_thought_steps = [
114-
"Let's think step by step." # zero shot prompt
115-
]
114+
if zero_shot:
115+
chain_of_thought_steps = [
116+
"Let's think step by step." # zero shot prompt
117+
]
118+
else:
119+
chain_of_thought_steps = [
120+
"Explore the API by reviewing any available documentation to learn about the API endpoints, data models, and behaviors.",
121+
"Identify all available endpoints (e.g., `/posts`, `/comments`, `/albums`, etc.).",
122+
"Use a tool like Postman or curl to interact with the API by making GET, POST, PUT, DELETE requests to understand the responses.",
123+
"Note down the response structures, status codes, and headers for each endpoint.",
124+
"For each endpoint, document the following details: URL, HTTP method (GET, POST, etc.), query parameters and path variables, expected request body structure for POST and PUT requests, response structure for successful and error responses.",
125+
"Identify common data structures returned by various endpoints and define them as reusable schemas. Determine the type of each field (e.g., integer, string, array) and define common response structures as components that can be referenced in multiple endpoint definitions.",
126+
"Create an OpenAPI document including metadata such as API title, version, and description, define the base URL of the API, list all endpoints, methods, parameters, and responses, and define reusable schemas, response types, and parameters.",
127+
"Ensure the correctness and completeness of the OpenAPI specification by validating the syntax and completeness of the document using tools like Swagger Editor, and ensure the specification matches the actual behavior of the API.",
128+
"Refine the document based on feedback and additional testing, share the draft with others, gather feedback, and make necessary adjustments. Regularly update the specification as the API evolves.",
129+
"Make the OpenAPI specification available to developers by incorporating it into your API documentation site and keep the documentation up to date with API changes."
130+
]
131+
132+
116133
#if previous_prompt == "Not a valid flag":
117134
# return previous_prompt
118135
return "\n".join([previous_prompt] + chain_of_thought_steps)

‎usecases/web_api_testing/simple_openapi_documentation.py‎

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import datetime
2+
import os
13
import time
24
from dataclasses import dataclass, field
35
from typing import List, Any, Union, Dict
@@ -16,6 +18,7 @@
1618
from usecases import use_case
1719
from usecases.usecase.roundbased import RoundBasedUseCase
1820
import pydantic_core
21+
import yaml
1922

2023
Prompt = List[Union[ChatCompletionMessage, ChatCompletionMessageParam]]
2124
Context = Any
@@ -30,6 +33,7 @@ class SimpleWebAPIDocumentation(RoundBasedUseCase):
3033
_context: Context = field(default_factory=lambda: {"notes": list()})
3134
_capabilities: Dict[str, Capability] = field(default_factory=dict)
3235
_all_http_methods_found: bool = False
36+
3337
# Parameter specifying the pattern description for expected HTTP methods in the API response
3438
http_method_description: str = parameter(
3539
desc="Pattern description for expected HTTP methods in the API response",
@@ -47,22 +51,33 @@ class SimpleWebAPIDocumentation(RoundBasedUseCase):
4751
desc="Comma-separated list of HTTP methods expected to be used in the API response.",
4852
default="GET,POST,PUT,PATCH,DELETE"
4953
)
54+
5055
def init(self):
5156
super().init()
57+
self.openapi_spec = self.openapi_spec = {
58+
"openapi": "3.0.0",
59+
"info": {
60+
"title": "Generated API Documentation",
61+
"version": "1.0",
62+
"description": "Automatically generated description of the API."
63+
},
64+
"servers": [{"url": "https://jsonplaceholder.typicode.com"}],
65+
"paths": {}
66+
}
5267
self._prompt_history.append(
5368
{
5469
"role": "system",
5570
"content": f"You're tasked with documenting the REST APIs of a website hosted at {self.host}. "
5671
f"Your main goal is to comprehensively explore the APIs endpoints and responses, and then document your findings in form of a OpenAPI specification."
57-
f" This thorough documentation will facilitate analysis later on.\n"
72+
f"Start with an empty OpenAPI specification. This thorough documentation will facilitate analysis later on.\n"
5873
f"Maintain meticulousness in documenting your observations as you traverse the APIs. This will streamline the documentation process.\n"
5974
f"Avoid resorting to brute-force methods. All essential information should be accessible through the API endpoints.\n"
6075

6176
})
6277
self.prompt_engineer = PromptEngineer(
63-
strategy=PromptStrategy.CHAIN_OF_THOUGHT,
64-
api_key=self.llm.api_key,
65-
history=self._prompt_history)
78+
strategy=PromptStrategy.CHAIN_OF_THOUGHT,
79+
api_key=self.llm.api_key,
80+
history=self._prompt_history)
6681

6782
self._context["host"] = self.host
6883
sett = set(self.http_method_template.format(method=method) for method in self.http_methods.split(","))
@@ -73,13 +88,13 @@ def init(self):
7388
"http_request": HTTPRequest(self.host),
7489
"record_note": RecordNote(self._context["notes"]),
7590
}
91+
self.current_time = datetime.datetime.now()
7692

7793
def all_http_methods_found(self):
7894
self.console.print(Panel("All HTTP methods found! Congratulations!", title="system"))
7995
self._all_http_methods_found = True
8096

81-
def perform_round(self, turn: int):
82-
97+
def perform_round(self, turn: int, FINAL_ROUND=20):
8398

8499
with self.console.status("[bold green]Asking LLM for a new command..."):
85100
# generate prompt
@@ -101,20 +116,24 @@ def perform_round(self, turn: int):
101116
self._prompt_history.append(message)
102117
content = completion.choices[0].message.content
103118

104-
#print(f'message:{message}')
119+
# print(f'message:{message}')
105120
answer = LLMResult(content, str(prompt),
106121
content, toc - tic, completion.usage.prompt_tokens,
107122
completion.usage.completion_tokens)
108-
#print(f'answer: {answer}')
123+
# print(f'answer: {answer}')
109124

110125
with self.console.status("[bold green]Executing that command..."):
111126
result = response.execute()
112127

113128
self.console.print(Panel(result, title="tool"))
114129
result_str = self.parse_http_status_line(result)
115130
self._prompt_history.append(tool_message(result_str, tool_call_id))
131+
if result_str == '200 OK':
132+
self.update_openapi_spec(response )
116133

117134
self.log_db.add_log_query(self._run_id, turn, command, result, answer)
135+
# if self._all_http_methods_found or turn == FINAL_ROUND:
136+
self.write_openapi_to_yaml()
118137
return self._all_http_methods_found
119138

120139
def parse_http_status_line(self, status_line):
@@ -130,10 +149,74 @@ def parse_http_status_line(self, status_line):
130149
status_code = parts[1] # e.g., "200"
131150
status_message = parts[2].split("\r\n")[0] # e.g., "OK"
132151
print(f'status code:{status_code}, status msg:{status_message}')
133-
return str(status_code +" " + status_message )
152+
return str(status_code + " " + status_message)
134153
else:
135154
raise ValueError("Invalid HTTP status line")
136155

137-
138-
139-
156+
def has_no_numbers(self,path):
157+
for char in path:
158+
if char.isdigit():
159+
return False
160+
return True
161+
def update_openapi_spec(self, response):
162+
# This function should parse the request and update the OpenAPI specification
163+
# For the purpose of this example, let's assume it parses JSON requests and updates paths
164+
request = response.action
165+
path = request.path
166+
method = request.method
167+
if path and method:
168+
if path not in self.openapi_spec['paths'] :#and self.has_no_numbers(path):
169+
self.openapi_spec['paths'][path] = {}
170+
self.openapi_spec['paths'][path][method.lower()] = {
171+
"summary": f"{method} operation on {path}",
172+
"responses": {
173+
"200": {
174+
"description": "Successful response",
175+
"content": {
176+
"application/json": {
177+
"schema": {"type": "object"} # Simplified for example
178+
}
179+
}
180+
}
181+
}
182+
}
183+
184+
def write_openapi_to_yaml(self, filename='openapi_spec.yaml'):
185+
"""Write the OpenAPI specification to a YAML file."""
186+
try:
187+
openapi_data = {
188+
"openapi": self.openapi_spec["openapi"],
189+
"info": self.openapi_spec["info"],
190+
"servers": self.openapi_spec["servers"],
191+
"paths": self.openapi_spec["paths"]
192+
}
193+
194+
# Ensure the directory exists
195+
file_path = filename.split(".yaml")[0]
196+
file_name = filename.split(".yaml")[0] + "_"+ self.current_time.strftime("%Y-%m-%d %H:%M:%S")+".yaml"
197+
os.makedirs(file_path, exist_ok=True)
198+
199+
with open(os.path.join(file_path, file_name), 'w') as yaml_file:
200+
yaml.dump(openapi_data, yaml_file, allow_unicode=True, default_flow_style=False)
201+
self.console.print(f"[green]OpenAPI specification written to [bold]{filename}[/bold].")
202+
except Exception as e:
203+
raise Exception(e)
204+
205+
#self.console.print(f"[red]Error writing YAML file: {e}")
206+
def write_openapi_to_yaml2(self, filename='openapi_spec.yaml'):
207+
"""Write the OpenAPI specification to a YAML file."""
208+
try:
209+
# self.setup_yaml() # Configure YAML to handle complex types
210+
with open(filename, 'w') as yaml_file:
211+
yaml.dump(self.openapi_spec, yaml_file, allow_unicode=True, default_flow_style=False)
212+
self.console.print(f"[green]OpenAPI specification written to [bold]{filename}[/bold].")
213+
except TypeError as e:
214+
raise Exception(e)
215+
#self.console.print(f"[red]Error writing YAML file: {e}")
216+
217+
def represent_dict_order(self, data):
218+
return self.represent_mapping('tag:yaml.org,2002:map', data.items())
219+
220+
def setup_yaml(self):
221+
"""Configure YAML to output OrderedDicts as regular dicts (helpful for better YAML readability)."""
222+
yaml.add_representer(dict, self.represent_dict_order)

0 commit comments

Comments
 (0)