postman-agent 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- postman_agent-1.0.0/MANIFEST.in +0 -0
- postman_agent-1.0.0/PKG-INFO +12 -0
- postman_agent-1.0.0/README.md +1 -0
- postman_agent-1.0.0/postman_agent/__init__.py +3 -0
- postman_agent-1.0.0/postman_agent/agent/__init__.py +3 -0
- postman_agent-1.0.0/postman_agent/agent/graph.py +329 -0
- postman_agent-1.0.0/postman_agent/cli.py +241 -0
- postman_agent-1.0.0/postman_agent/setup.py +42 -0
- postman_agent-1.0.0/postman_agent/tools/__init__.py +3 -0
- postman_agent-1.0.0/postman_agent/tools/code_parser.py +105 -0
- postman_agent-1.0.0/postman_agent.egg-info/PKG-INFO +12 -0
- postman_agent-1.0.0/postman_agent.egg-info/SOURCES.txt +17 -0
- postman_agent-1.0.0/postman_agent.egg-info/dependency_links.txt +1 -0
- postman_agent-1.0.0/postman_agent.egg-info/entry_points.txt +2 -0
- postman_agent-1.0.0/postman_agent.egg-info/requires.txt +8 -0
- postman_agent-1.0.0/postman_agent.egg-info/top_level.txt +1 -0
- postman_agent-1.0.0/pyproject.toml +25 -0
- postman_agent-1.0.0/setup.cfg +4 -0
- postman_agent-1.0.0/setup.py +12 -0
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postman-agent
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Requires-Python: >=3.9
|
|
5
|
+
Requires-Dist: fastapi
|
|
6
|
+
Requires-Dist: uvicorn
|
|
7
|
+
Requires-Dist: langchain
|
|
8
|
+
Requires-Dist: langchain-groq
|
|
9
|
+
Requires-Dist: langgraph
|
|
10
|
+
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"# postman-ai-agent"
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
import re
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from langchain_groq import ChatGroq
|
|
8
|
+
from langgraph.graph import StateGraph, END
|
|
9
|
+
from langgraph.graph.message import add_messages
|
|
10
|
+
from typing_extensions import TypedDict
|
|
11
|
+
from pydantic import SecretStr
|
|
12
|
+
from langchain.tools import tool
|
|
13
|
+
import httpx
|
|
14
|
+
from postman_agent.tools.code_parser import parse_code_tool
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
# ─── State ────────────────────────────────────────────────────────────────────
|
|
19
|
+
class AgentState(TypedDict):
|
|
20
|
+
messages: Annotated[list, add_messages]
|
|
21
|
+
code: str
|
|
22
|
+
routes: list
|
|
23
|
+
postman_collection: dict
|
|
24
|
+
postman_import: dict
|
|
25
|
+
|
|
26
|
+
# ─── Postman Import Tool ──────────────────────────────────────────────────────
|
|
27
|
+
@tool
|
|
28
|
+
def import_to_postman_tool(collection_json: str) -> dict:
|
|
29
|
+
"""Imports a Postman Collection JSON directly into Postman workspace"""
|
|
30
|
+
api_key = os.getenv("POSTMAN_API_KEY")
|
|
31
|
+
if not api_key:
|
|
32
|
+
return {"success": False, "error": "POSTMAN_API_KEY not found in environment"}
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
collection_data = json.loads(collection_json)
|
|
36
|
+
except json.JSONDecodeError as e:
|
|
37
|
+
return {"success": False, "error": f"Invalid JSON: {str(e)}"}
|
|
38
|
+
|
|
39
|
+
headers = {
|
|
40
|
+
"X-Api-Key": api_key,
|
|
41
|
+
"Content-Type": "application/json"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if "info" in collection_data:
|
|
45
|
+
collection_data["info"].pop("_postman_id", None)
|
|
46
|
+
|
|
47
|
+
payload = {"collection": collection_data}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = httpx.post(
|
|
51
|
+
"https://api.getpostman.com/collections",
|
|
52
|
+
headers=headers,
|
|
53
|
+
json=payload,
|
|
54
|
+
timeout=30
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if response.status_code == 200:
|
|
58
|
+
data = response.json()
|
|
59
|
+
collection_id = data.get("collection", {}).get("uid", "")
|
|
60
|
+
collection_name = data.get("collection", {}).get("name", "")
|
|
61
|
+
return {
|
|
62
|
+
"success": True,
|
|
63
|
+
"message": f"Collection '{collection_name}' imported successfully!",
|
|
64
|
+
"collection_id": collection_id,
|
|
65
|
+
"postman_url": f"https://go.postman.co/collection/{collection_id}"
|
|
66
|
+
}
|
|
67
|
+
else:
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": f"Postman API error: {response.status_code} - {response.text}"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
except httpx.TimeoutException:
|
|
74
|
+
return {"success": False, "error": "Request timeout"}
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return {"success": False, "error": str(e)}
|
|
77
|
+
|
|
78
|
+
# ─── LLM Setup ────────────────────────────────────────────────────────────────
|
|
79
|
+
llm = ChatGroq(
|
|
80
|
+
api_key=SecretStr(os.getenv("GROQ_API_KEY") or ""),
|
|
81
|
+
model="llama-3.1-8b-instant",
|
|
82
|
+
temperature=0
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# ─── Direct Parser ────────────────────────────────────────────────────────────
|
|
86
|
+
def extract_routes_directly(code: str) -> list:
|
|
87
|
+
"""Directly extract routes from code without LLM"""
|
|
88
|
+
routes = []
|
|
89
|
+
seen = set()
|
|
90
|
+
|
|
91
|
+
express_pattern = r'(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'
|
|
92
|
+
python_pattern = r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'
|
|
93
|
+
flask_pattern = r'@\w+\.route\s*\(\s*["\']([^"\']+)["\'].*?methods\s*=\s*\[([^\]]+)\]'
|
|
94
|
+
django_pattern = r'(?:path|re_path)\s*\(\s*["\']([^"\']+)["\']'
|
|
95
|
+
|
|
96
|
+
# ─── Python AST ───────────────────────────────────────────────────────
|
|
97
|
+
try:
|
|
98
|
+
import ast
|
|
99
|
+
tree = ast.parse(code)
|
|
100
|
+
for node in ast.walk(tree):
|
|
101
|
+
if isinstance(node, ast.FunctionDef):
|
|
102
|
+
for decorator in node.decorator_list:
|
|
103
|
+
if isinstance(decorator, ast.Call):
|
|
104
|
+
func = decorator.func
|
|
105
|
+
if isinstance(func, ast.Attribute):
|
|
106
|
+
method = func.attr.lower()
|
|
107
|
+
if method in ["get", "post", "put", "delete", "patch"]:
|
|
108
|
+
if decorator.args:
|
|
109
|
+
path_node = decorator.args[0]
|
|
110
|
+
if isinstance(path_node, ast.Constant):
|
|
111
|
+
key = f"{method.upper()}:{path_node.value}"
|
|
112
|
+
if key not in seen:
|
|
113
|
+
seen.add(key)
|
|
114
|
+
routes.append({
|
|
115
|
+
"method": method.upper(),
|
|
116
|
+
"path": path_node.value,
|
|
117
|
+
"name": node.name,
|
|
118
|
+
"description": f"{method.upper()} {path_node.value}"
|
|
119
|
+
})
|
|
120
|
+
except SyntaxError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# ─── Express.js ───────────────────────────────────────────────────────
|
|
124
|
+
if not routes:
|
|
125
|
+
matches = re.findall(express_pattern, code, re.IGNORECASE | re.MULTILINE)
|
|
126
|
+
for method, path in matches:
|
|
127
|
+
if not path.startswith('/'):
|
|
128
|
+
continue
|
|
129
|
+
key = f"{method.upper()}:{path}"
|
|
130
|
+
if key not in seen:
|
|
131
|
+
seen.add(key)
|
|
132
|
+
name = path.replace('/', ' ').replace(':', '').replace('-', ' ').strip()
|
|
133
|
+
name = ' '.join(w.capitalize() for w in name.split()) or "Endpoint"
|
|
134
|
+
routes.append({
|
|
135
|
+
"method": method.upper(),
|
|
136
|
+
"path": path,
|
|
137
|
+
"name": name,
|
|
138
|
+
"description": f"{method.upper()} {path}"
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# ─── Flask ────────────────────────────────────────────────────────────
|
|
142
|
+
if not routes:
|
|
143
|
+
matches = re.findall(flask_pattern, code, re.IGNORECASE | re.DOTALL)
|
|
144
|
+
for path, methods_str in matches:
|
|
145
|
+
methods = [m.strip().strip('"\'') for m in methods_str.split(',')]
|
|
146
|
+
for method in methods:
|
|
147
|
+
if method.upper() in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
|
148
|
+
key = f"{method.upper()}:{path}"
|
|
149
|
+
if key not in seen:
|
|
150
|
+
seen.add(key)
|
|
151
|
+
routes.append({
|
|
152
|
+
"method": method.upper(),
|
|
153
|
+
"path": path,
|
|
154
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
155
|
+
"description": f"{method.upper()} {path}"
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
# ─── Django ───────────────────────────────────────────────────────────
|
|
159
|
+
if not routes:
|
|
160
|
+
matches = re.findall(django_pattern, code, re.IGNORECASE)
|
|
161
|
+
for path in matches:
|
|
162
|
+
key = f"GET:/{path}"
|
|
163
|
+
if key not in seen:
|
|
164
|
+
seen.add(key)
|
|
165
|
+
routes.append({
|
|
166
|
+
"method": "GET",
|
|
167
|
+
"path": f"/{path}",
|
|
168
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
169
|
+
"description": f"GET /{path}"
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
# ─── Python decorator fallback ────────────────────────────────────────
|
|
173
|
+
if not routes:
|
|
174
|
+
matches = re.findall(python_pattern, code, re.IGNORECASE)
|
|
175
|
+
for method, path in matches:
|
|
176
|
+
key = f"{method.upper()}:{path}"
|
|
177
|
+
if key not in seen:
|
|
178
|
+
seen.add(key)
|
|
179
|
+
routes.append({
|
|
180
|
+
"method": method.upper(),
|
|
181
|
+
"path": path,
|
|
182
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
183
|
+
"description": f"{method.upper()} {path}"
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
print(f"🔧 Direct parser found: {len(routes)} routes")
|
|
187
|
+
return routes
|
|
188
|
+
|
|
189
|
+
# ─── Build Postman Collection ─────────────────────────────────────────────────
|
|
190
|
+
def generate_sample_body(path: str, method: str) -> dict:
|
|
191
|
+
path_lower = path.lower()
|
|
192
|
+
if "auth" in path_lower or "login" in path_lower:
|
|
193
|
+
return {"email": "user@example.com", "password": "password123"}
|
|
194
|
+
elif "register" in path_lower:
|
|
195
|
+
return {"name": "John Doe", "email": "user@example.com", "password": "password123"}
|
|
196
|
+
elif "product" in path_lower:
|
|
197
|
+
return {"name": "Product Name", "price": 99.99, "category": "electronics", "stock": 100}
|
|
198
|
+
elif "order" in path_lower:
|
|
199
|
+
return {"products": [{"id": "product_id", "quantity": 1}], "address": "123 Street"}
|
|
200
|
+
elif "cart" in path_lower:
|
|
201
|
+
return {"productId": "product_id", "quantity": 1}
|
|
202
|
+
elif "user" in path_lower or "profile" in path_lower:
|
|
203
|
+
return {"name": "John Doe", "email": "user@example.com", "phone": "1234567890"}
|
|
204
|
+
elif "category" in path_lower:
|
|
205
|
+
return {"name": "Category Name", "description": "Category description"}
|
|
206
|
+
elif "review" in path_lower:
|
|
207
|
+
return {"rating": 5, "comment": "Great product!"}
|
|
208
|
+
elif "address" in path_lower:
|
|
209
|
+
return {"street": "123 Main St", "city": "Karachi", "country": "Pakistan"}
|
|
210
|
+
elif "coupon" in path_lower:
|
|
211
|
+
return {"code": "DISCOUNT10", "discount": 10, "type": "percentage"}
|
|
212
|
+
elif "payment" in path_lower:
|
|
213
|
+
return {"amount": 1000, "currency": "PKR", "method": "card"}
|
|
214
|
+
else:
|
|
215
|
+
return {"key": "value"}
|
|
216
|
+
|
|
217
|
+
def build_postman_collection(routes: list) -> dict:
|
|
218
|
+
groups = {}
|
|
219
|
+
for route in routes:
|
|
220
|
+
path = route.get("path", "/")
|
|
221
|
+
parts = [p for p in path.split("/") if p and not p.startswith(":")]
|
|
222
|
+
group = parts[0].capitalize() if parts else "General"
|
|
223
|
+
if group not in groups:
|
|
224
|
+
groups[group] = []
|
|
225
|
+
groups[group].append(route)
|
|
226
|
+
|
|
227
|
+
items = []
|
|
228
|
+
for group_name, group_routes in groups.items():
|
|
229
|
+
group_items = []
|
|
230
|
+
for route in group_routes:
|
|
231
|
+
method = route.get("method", "GET")
|
|
232
|
+
path = route.get("path", "/")
|
|
233
|
+
name = route.get("name", path)
|
|
234
|
+
path_parts = [p for p in path.split("/") if p]
|
|
235
|
+
|
|
236
|
+
item = {
|
|
237
|
+
"name": name,
|
|
238
|
+
"request": {
|
|
239
|
+
"method": method,
|
|
240
|
+
"header": [
|
|
241
|
+
{"key": "Content-Type", "value": "application/json"},
|
|
242
|
+
{"key": "Authorization", "value": "Bearer {{token}}"}
|
|
243
|
+
],
|
|
244
|
+
"url": {
|
|
245
|
+
"raw": "{{base_url}}" + path,
|
|
246
|
+
"host": ["{{base_url}}"],
|
|
247
|
+
"path": path_parts
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if method in ["POST", "PUT", "PATCH"]:
|
|
253
|
+
sample_body = generate_sample_body(path, method)
|
|
254
|
+
item["request"]["body"] = {
|
|
255
|
+
"mode": "raw",
|
|
256
|
+
"raw": json.dumps(sample_body, indent=2),
|
|
257
|
+
"options": {"raw": {"language": "json"}}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
group_items.append(item)
|
|
261
|
+
|
|
262
|
+
items.append({"name": group_name, "item": group_items})
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"info": {
|
|
266
|
+
"name": "AI Generated API Collection",
|
|
267
|
+
"_postman_id": str(uuid.uuid4()),
|
|
268
|
+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
269
|
+
"description": "Auto-generated by Postman AI Agent"
|
|
270
|
+
},
|
|
271
|
+
"item": items,
|
|
272
|
+
"variable": [
|
|
273
|
+
{"key": "base_url", "value": "http://localhost:3000"},
|
|
274
|
+
{"key": "token", "value": "your_jwt_token_here"}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ─── Nodes ────────────────────────────────────────────────────────────────────
|
|
279
|
+
def agent_node(state: AgentState) -> AgentState:
|
|
280
|
+
print("🤖 Agent node called!")
|
|
281
|
+
print(f"📝 Code length: {len(state['code'])}")
|
|
282
|
+
routes = extract_routes_directly(state["code"])
|
|
283
|
+
from langchain_core.messages import HumanMessage
|
|
284
|
+
return {
|
|
285
|
+
"messages": [HumanMessage(content=f"Extracted {len(routes)} routes")],
|
|
286
|
+
"code": state["code"],
|
|
287
|
+
"routes": routes,
|
|
288
|
+
"postman_collection": {},
|
|
289
|
+
"postman_import": {}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def generate_postman_node(state: AgentState) -> AgentState:
|
|
293
|
+
print("📦 Generating Postman Collection...")
|
|
294
|
+
routes = state.get("routes", [])
|
|
295
|
+
print(f"🔍 Found {len(routes)} routes")
|
|
296
|
+
|
|
297
|
+
postman_collection = build_postman_collection(routes)
|
|
298
|
+
print("✅ Postman collection generated!")
|
|
299
|
+
|
|
300
|
+
print("🚀 Importing to Postman...")
|
|
301
|
+
postman_result = {"success": False}
|
|
302
|
+
import_result = import_to_postman_tool.invoke(json.dumps(postman_collection))
|
|
303
|
+
postman_result = import_result
|
|
304
|
+
|
|
305
|
+
if postman_result.get("success"):
|
|
306
|
+
print(f"✅ Imported to Postman: {postman_result.get('postman_url')}")
|
|
307
|
+
else:
|
|
308
|
+
print(f"❌ Postman import failed: {postman_result.get('error')}")
|
|
309
|
+
|
|
310
|
+
from langchain_core.messages import HumanMessage
|
|
311
|
+
return {
|
|
312
|
+
"messages": [HumanMessage(content="Collection generated!")],
|
|
313
|
+
"code": state["code"],
|
|
314
|
+
"postman_collection": postman_collection,
|
|
315
|
+
"routes": routes,
|
|
316
|
+
"postman_import": postman_result
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# ─── Build Graph ──────────────────────────────────────────────────────────────
|
|
320
|
+
def create_agent():
|
|
321
|
+
graph = StateGraph(AgentState)
|
|
322
|
+
graph.add_node("agent", agent_node)
|
|
323
|
+
graph.add_node("generate", generate_postman_node)
|
|
324
|
+
graph.set_entry_point("agent")
|
|
325
|
+
graph.add_edge("agent", "generate")
|
|
326
|
+
graph.add_edge("generate", END)
|
|
327
|
+
return graph.compile()
|
|
328
|
+
|
|
329
|
+
agent = create_agent()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
def load_env():
|
|
10
|
+
"""Load .env from multiple possible locations"""
|
|
11
|
+
possible_paths = [
|
|
12
|
+
Path.cwd() / ".env",
|
|
13
|
+
Path.home() / ".postman-agent" / ".env",
|
|
14
|
+
Path(__file__).parent.parent / ".env",
|
|
15
|
+
]
|
|
16
|
+
for env_path in possible_paths:
|
|
17
|
+
if env_path.exists():
|
|
18
|
+
load_dotenv(env_path)
|
|
19
|
+
return str(env_path)
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def print_banner():
|
|
23
|
+
print("""
|
|
24
|
+
╔═══════════════════════════════════════╗
|
|
25
|
+
║ 🤖 Postman AI Agent CLI ║
|
|
26
|
+
║ LangGraph + Groq + Postman API ║
|
|
27
|
+
╚═══════════════════════════════════════╝
|
|
28
|
+
""")
|
|
29
|
+
|
|
30
|
+
def detect_framework(code: str) -> str:
|
|
31
|
+
if "express" in code.lower() or "router." in code.lower():
|
|
32
|
+
return "Express.js"
|
|
33
|
+
elif "fastapi" in code.lower():
|
|
34
|
+
return "FastAPI"
|
|
35
|
+
elif "flask" in code.lower():
|
|
36
|
+
return "Flask"
|
|
37
|
+
elif "django" in code.lower() or "urlpatterns" in code.lower():
|
|
38
|
+
return "Django"
|
|
39
|
+
elif "fastify" in code.lower():
|
|
40
|
+
return "Fastify"
|
|
41
|
+
return "Unknown"
|
|
42
|
+
|
|
43
|
+
def is_api_file(file_path: Path) -> bool:
|
|
44
|
+
"""File naam se nahi — content se detect karo"""
|
|
45
|
+
api_patterns = [
|
|
46
|
+
r'(?:router|app)\.(get|post|put|delete|patch)\s*\(',
|
|
47
|
+
r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(',
|
|
48
|
+
r'@\w+\.route\s*\(',
|
|
49
|
+
r'(?:path|re_path|url)\s*\(\s*["\']',
|
|
50
|
+
r'fastify\.(get|post|put|delete|patch)\s*\(',
|
|
51
|
+
r'app\.use\s*\(\s*["\']\/',
|
|
52
|
+
]
|
|
53
|
+
try:
|
|
54
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
55
|
+
for pattern in api_patterns:
|
|
56
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
57
|
+
return True
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
async def run_agent(code: str):
|
|
63
|
+
from postman_agent.agent.graph import agent
|
|
64
|
+
initial_state = {
|
|
65
|
+
"messages": [],
|
|
66
|
+
"code": code,
|
|
67
|
+
"routes": [],
|
|
68
|
+
"postman_collection": {},
|
|
69
|
+
"postman_import": {}
|
|
70
|
+
}
|
|
71
|
+
result = await agent.ainvoke(initial_state)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
def setup_command(args):
|
|
75
|
+
"""Save API keys to .env file"""
|
|
76
|
+
print_banner()
|
|
77
|
+
print("⚙️ Setup — API Keys Configuration\n")
|
|
78
|
+
|
|
79
|
+
groq_key = input("🔑 Enter your GROQ API Key: ").strip()
|
|
80
|
+
postman_key = input("🔑 Enter your POSTMAN API Key: ").strip()
|
|
81
|
+
|
|
82
|
+
# ─── Save to home directory ───────────────────────────────────────────
|
|
83
|
+
env_dir = Path.home() / ".postman-agent"
|
|
84
|
+
env_dir.mkdir(exist_ok=True)
|
|
85
|
+
env_file = env_dir / ".env"
|
|
86
|
+
|
|
87
|
+
with open(env_file, "w") as f:
|
|
88
|
+
f.write(f"GROQ_API_KEY={groq_key}\n")
|
|
89
|
+
f.write(f"POSTMAN_API_KEY={postman_key}\n")
|
|
90
|
+
|
|
91
|
+
print(f"\n✅ API keys saved to: {env_file}")
|
|
92
|
+
print("🚀 You're ready! Run: postman-agent generate --scan .\n")
|
|
93
|
+
|
|
94
|
+
def generate_command(args):
|
|
95
|
+
"""Main generate command"""
|
|
96
|
+
print_banner()
|
|
97
|
+
|
|
98
|
+
# ─── Load env ─────────────────────────────────────────────────────────
|
|
99
|
+
env_path = load_env()
|
|
100
|
+
|
|
101
|
+
# ─── Check API keys ───────────────────────────────────────────────────
|
|
102
|
+
if not os.getenv("GROQ_API_KEY"):
|
|
103
|
+
print("❌ GROQ_API_KEY not found!")
|
|
104
|
+
print("💡 Run: postman-agent setup")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
if not os.getenv("POSTMAN_API_KEY"):
|
|
108
|
+
print("❌ POSTMAN_API_KEY not found!")
|
|
109
|
+
print("💡 Run: postman-agent setup")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
# ─── Excluded folders ─────────────────────────────────────────────────
|
|
113
|
+
excluded = [
|
|
114
|
+
"node_modules", "venv", "__pycache__",
|
|
115
|
+
".git", "dist", "build", ".next",
|
|
116
|
+
"coverage", "vendor", "env", ".venv"
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
if args.file:
|
|
120
|
+
file_path = Path(args.file)
|
|
121
|
+
if not file_path.exists():
|
|
122
|
+
print(f"❌ File not found: {args.file}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
print(f"📝 Reading file: {args.file}")
|
|
125
|
+
code = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
126
|
+
|
|
127
|
+
elif args.scan:
|
|
128
|
+
scan_path = Path(args.scan)
|
|
129
|
+
if not scan_path.exists():
|
|
130
|
+
print(f"❌ Directory not found: {args.scan}")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
print(f"🔍 Scanning directory: {args.scan}")
|
|
134
|
+
|
|
135
|
+
extensions = [".js", ".ts", ".py"]
|
|
136
|
+
all_files = []
|
|
137
|
+
for ext in extensions:
|
|
138
|
+
all_files.extend(scan_path.rglob(f"*{ext}"))
|
|
139
|
+
|
|
140
|
+
all_files = [
|
|
141
|
+
f for f in all_files
|
|
142
|
+
if not any(ex in str(f) for ex in excluded)
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
files = [f for f in all_files if is_api_file(f)]
|
|
146
|
+
|
|
147
|
+
if not files:
|
|
148
|
+
print("❌ No API files found!")
|
|
149
|
+
print("💡 Try: postman-agent generate --file path/to/routes.js")
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
print(f"📁 Found {len(files)} API files:")
|
|
153
|
+
for f in files:
|
|
154
|
+
print(f" → {f.name}")
|
|
155
|
+
|
|
156
|
+
code = ""
|
|
157
|
+
for f in files:
|
|
158
|
+
code += f"\n// ═══ File: {f.name} ═══\n"
|
|
159
|
+
code += f.read_text(encoding="utf-8", errors="ignore")
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
print("❌ Please provide --file or --scan")
|
|
163
|
+
print(" Example: postman-agent generate --file routes.js")
|
|
164
|
+
print(" Example: postman-agent generate --scan .")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
framework = detect_framework(code)
|
|
168
|
+
print(f"🔎 Detected Framework: {framework}")
|
|
169
|
+
print(f"📊 Code size: {len(code)} characters")
|
|
170
|
+
print(f"\n🤖 AI Agent analyzing code...")
|
|
171
|
+
print("⏳ Please wait...\n")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result = asyncio.run(run_agent(code))
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"❌ Agent error: {str(e)}")
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
routes = result.get("routes", [])
|
|
180
|
+
postman_import = result.get("postman_import", {})
|
|
181
|
+
postman_collection = result.get("postman_collection", {})
|
|
182
|
+
|
|
183
|
+
print(f"✅ Found {len(routes)} routes!")
|
|
184
|
+
|
|
185
|
+
if routes:
|
|
186
|
+
print("\n📋 Detected Routes:")
|
|
187
|
+
for route in routes:
|
|
188
|
+
method = route.get("method", "GET")
|
|
189
|
+
path = route.get("path", "/")
|
|
190
|
+
print(f" {method:<8} {path}")
|
|
191
|
+
|
|
192
|
+
print(f"\n📦 Postman Collection generated!")
|
|
193
|
+
|
|
194
|
+
if postman_import.get("success"):
|
|
195
|
+
print(f"\n🚀 ✅ Imported to Postman Successfully!")
|
|
196
|
+
print(f"📌 Collection: {postman_collection.get('info', {}).get('name', 'API Collection')}")
|
|
197
|
+
print(f"🔗 Open here: {postman_import.get('postman_url')}")
|
|
198
|
+
else:
|
|
199
|
+
print(f"\n⚠️ Postman Import Failed: {postman_import.get('error', 'Unknown error')}")
|
|
200
|
+
print("💡 Run: postman-agent setup")
|
|
201
|
+
|
|
202
|
+
print("\n✨ Done! Happy Testing! 🎉\n")
|
|
203
|
+
|
|
204
|
+
def main():
|
|
205
|
+
parser = argparse.ArgumentParser(
|
|
206
|
+
description="🤖 Postman AI Agent — Auto generate Postman Collections from code",
|
|
207
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
208
|
+
epilog="""
|
|
209
|
+
Commands:
|
|
210
|
+
setup Save your API keys (run once)
|
|
211
|
+
generate Generate Postman Collection
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
postman-agent setup
|
|
215
|
+
postman-agent generate --file routes.js
|
|
216
|
+
postman-agent generate --scan .
|
|
217
|
+
postman-agent generate --scan ./src
|
|
218
|
+
"""
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
222
|
+
|
|
223
|
+
# ─── Setup command ────────────────────────────────────────────────────
|
|
224
|
+
subparsers.add_parser("setup", help="Save API keys (run once)")
|
|
225
|
+
|
|
226
|
+
# ─── Generate command ─────────────────────────────────────────────────
|
|
227
|
+
gen_parser = subparsers.add_parser("generate", help="Generate Postman Collection")
|
|
228
|
+
gen_parser.add_argument("--file", "-f", help="Path to route file")
|
|
229
|
+
gen_parser.add_argument("--scan", "-s", help="Scan entire directory")
|
|
230
|
+
|
|
231
|
+
args = parser.parse_args()
|
|
232
|
+
|
|
233
|
+
if args.command == "setup":
|
|
234
|
+
setup_command(args)
|
|
235
|
+
elif args.command == "generate":
|
|
236
|
+
generate_command(args)
|
|
237
|
+
else:
|
|
238
|
+
parser.print_help()
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as f:
|
|
4
|
+
long_description = f.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="postman-ai-agent",
|
|
8
|
+
version="1.0.0",
|
|
9
|
+
author="M. Abubakar",
|
|
10
|
+
author_email="your-email@gmail.com",
|
|
11
|
+
description="AI Agent that auto-generates Postman Collections from backend code",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/Abubakar-webmaker/postman-ai-agent",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
python_requires=">=3.10",
|
|
17
|
+
install_requires=[
|
|
18
|
+
"langchain>=0.2.0",
|
|
19
|
+
"langgraph>=0.2.0",
|
|
20
|
+
"langchain-groq>=0.1.0",
|
|
21
|
+
"python-dotenv>=1.0.0",
|
|
22
|
+
"httpx>=0.27.0",
|
|
23
|
+
"typing-extensions>=4.0.0",
|
|
24
|
+
],
|
|
25
|
+
entry_points={
|
|
26
|
+
"console_scripts": [
|
|
27
|
+
"postman-agent=postman_agent.cli:main",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
classifiers=[
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"Topic :: Software Development :: Code Generators",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
36
|
+
],
|
|
37
|
+
keywords=[
|
|
38
|
+
"postman", "api", "ai-agent", "langgraph",
|
|
39
|
+
"groq", "code-generator", "rest-api", "express",
|
|
40
|
+
"fastapi", "flask", "django"
|
|
41
|
+
],
|
|
42
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
from langchain.tools import tool
|
|
5
|
+
|
|
6
|
+
@tool
|
|
7
|
+
def parse_code_tool(code: str) -> dict[str, Any]:
|
|
8
|
+
"""
|
|
9
|
+
Analyzes backend code and extracts API routes/endpoints.
|
|
10
|
+
Supports Express.js, FastAPI, Flask, and Django patterns.
|
|
11
|
+
"""
|
|
12
|
+
routes = []
|
|
13
|
+
|
|
14
|
+
# ─── Express.js Patterns ──────────────────────────────────────────────
|
|
15
|
+
express_pattern = r'(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'
|
|
16
|
+
use_pattern = r'app\.use\s*\(\s*["\']([^"\']+)["\']'
|
|
17
|
+
|
|
18
|
+
# ─── FastAPI / Flask Patterns ─────────────────────────────────────────
|
|
19
|
+
python_decorator_pattern = r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'
|
|
20
|
+
flask_route_pattern = r'@\w+\.route\s*\(\s*["\']([^"\']+)["\'].*?methods\s*=\s*\[([^\]]+)\]'
|
|
21
|
+
django_path_pattern = r'(?:path|re_path)\s*\(\s*["\']([^"\']+)["\']'
|
|
22
|
+
|
|
23
|
+
# ─── Try Python AST first ─────────────────────────────────────────────
|
|
24
|
+
try:
|
|
25
|
+
tree = ast.parse(code)
|
|
26
|
+
for node in ast.walk(tree):
|
|
27
|
+
if isinstance(node, ast.FunctionDef):
|
|
28
|
+
for decorator in node.decorator_list:
|
|
29
|
+
if isinstance(decorator, ast.Call):
|
|
30
|
+
func = decorator.func
|
|
31
|
+
if isinstance(func, ast.Attribute):
|
|
32
|
+
method = func.attr.lower()
|
|
33
|
+
if method in ["get", "post", "put", "delete", "patch"]:
|
|
34
|
+
if decorator.args:
|
|
35
|
+
path_node = decorator.args[0]
|
|
36
|
+
if isinstance(path_node, ast.Constant):
|
|
37
|
+
routes.append({
|
|
38
|
+
"method": method.upper(),
|
|
39
|
+
"path": path_node.value,
|
|
40
|
+
"name": node.name,
|
|
41
|
+
"description": ast.get_docstring(node) or f"{method.upper()} {path_node.value}"
|
|
42
|
+
})
|
|
43
|
+
except SyntaxError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# ─── Express.js Regex ─────────────────────────────────────────────────
|
|
47
|
+
if not routes:
|
|
48
|
+
seen = set()
|
|
49
|
+
matches = re.findall(express_pattern, code, re.IGNORECASE | re.MULTILINE)
|
|
50
|
+
for method, path in matches:
|
|
51
|
+
if not path.startswith('/'):
|
|
52
|
+
continue
|
|
53
|
+
key = f"{method.upper()}:{path}"
|
|
54
|
+
if key not in seen:
|
|
55
|
+
seen.add(key)
|
|
56
|
+
name = path.replace('/', ' ').replace(':', '').replace('-', ' ').strip()
|
|
57
|
+
name = ' '.join(w.capitalize() for w in name.split()) or "Endpoint"
|
|
58
|
+
routes.append({
|
|
59
|
+
"method": method.upper(),
|
|
60
|
+
"path": path,
|
|
61
|
+
"name": name,
|
|
62
|
+
"description": f"{method.upper()} {path}"
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
# ─── Flask Regex fallback ─────────────────────────────────────────────
|
|
66
|
+
if not routes:
|
|
67
|
+
matches = re.findall(flask_route_pattern, code, re.IGNORECASE | re.DOTALL)
|
|
68
|
+
for path, methods_str in matches:
|
|
69
|
+
methods = [m.strip().strip('"\'') for m in methods_str.split(',')]
|
|
70
|
+
for method in methods:
|
|
71
|
+
if method.upper() in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
|
72
|
+
routes.append({
|
|
73
|
+
"method": method.upper(),
|
|
74
|
+
"path": path,
|
|
75
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
76
|
+
"description": f"{method.upper()} {path}"
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
# ─── Django Regex fallback ────────────────────────────────────────────
|
|
80
|
+
if not routes:
|
|
81
|
+
matches = re.findall(django_path_pattern, code, re.IGNORECASE)
|
|
82
|
+
for path in matches:
|
|
83
|
+
routes.append({
|
|
84
|
+
"method": "GET",
|
|
85
|
+
"path": f"/{path}",
|
|
86
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
87
|
+
"description": f"GET /{path}"
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
# ─── Python decorator fallback ────────────────────────────────────────
|
|
91
|
+
if not routes:
|
|
92
|
+
matches = re.findall(python_decorator_pattern, code, re.IGNORECASE)
|
|
93
|
+
for method, path in matches:
|
|
94
|
+
routes.append({
|
|
95
|
+
"method": method.upper(),
|
|
96
|
+
"path": path,
|
|
97
|
+
"name": path.replace("/", "_").strip("_") or "endpoint",
|
|
98
|
+
"description": f"{method.upper()} {path}"
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"routes": routes,
|
|
103
|
+
"total": len(routes),
|
|
104
|
+
"status": "success" if routes else "no_routes_found"
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postman-agent
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Requires-Python: >=3.9
|
|
5
|
+
Requires-Dist: fastapi
|
|
6
|
+
Requires-Dist: uvicorn
|
|
7
|
+
Requires-Dist: langchain
|
|
8
|
+
Requires-Dist: langchain-groq
|
|
9
|
+
Requires-Dist: langgraph
|
|
10
|
+
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
postman_agent/__init__.py
|
|
6
|
+
postman_agent/cli.py
|
|
7
|
+
postman_agent/setup.py
|
|
8
|
+
postman_agent.egg-info/PKG-INFO
|
|
9
|
+
postman_agent.egg-info/SOURCES.txt
|
|
10
|
+
postman_agent.egg-info/dependency_links.txt
|
|
11
|
+
postman_agent.egg-info/entry_points.txt
|
|
12
|
+
postman_agent.egg-info/requires.txt
|
|
13
|
+
postman_agent.egg-info/top_level.txt
|
|
14
|
+
postman_agent/agent/__init__.py
|
|
15
|
+
postman_agent/agent/graph.py
|
|
16
|
+
postman_agent/tools/__init__.py
|
|
17
|
+
postman_agent/tools/code_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
postman_agent
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "postman-agent"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"fastapi",
|
|
11
|
+
"uvicorn",
|
|
12
|
+
"langchain",
|
|
13
|
+
"langchain-groq",
|
|
14
|
+
"langgraph",
|
|
15
|
+
"httpx",
|
|
16
|
+
"pydantic",
|
|
17
|
+
"python-dotenv",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
postman-agent = "postman_agent.cli:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["."]
|
|
25
|
+
include = ["postman_agent*"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="postman-agent",
|
|
5
|
+
version="1.0.0",
|
|
6
|
+
packages=find_packages(include=["postman_agent", "postman_agent.*"]),
|
|
7
|
+
entry_points={
|
|
8
|
+
"console_scripts": [
|
|
9
|
+
"postman-agent=postman_agent.cli:main",
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
)
|