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.
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,3 @@
1
+ __version__ = "1.0.0"
2
+ __author__ = "M. Abubakar"
3
+ __description__ = "AI Agent that auto-generates Postman Collections from backend code"
@@ -0,0 +1,3 @@
1
+ from postman_agent.agent.graph import agent
2
+
3
+ __all__ = ["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,3 @@
1
+ from postman_agent.tools.code_parser import parse_code_tool
2
+
3
+ __all__ = ["parse_code_tool"]
@@ -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,2 @@
1
+ [console_scripts]
2
+ postman-agent = postman_agent.cli:main
@@ -0,0 +1,8 @@
1
+ fastapi
2
+ uvicorn
3
+ langchain
4
+ langchain-groq
5
+ langgraph
6
+ httpx
7
+ pydantic
8
+ python-dotenv
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )