secondopinion-sdk 0.1.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.
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secondopinion-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the 2ndOpinion API
|
|
5
|
+
Project-URL: Homepage, https://get2ndopinion.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/brianmello8/2ndopinion
|
|
7
|
+
Author-email: 2ndOpinion <hello@get2ndopinion.dev>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: httpx>=0.24.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# secondopinion
|
|
17
|
+
|
|
18
|
+
Python SDK for the [2ndOpinion](https://get2ndopinion.dev) API — AI-to-AI code review.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install secondopinion-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from secondopinion import SecondOpinion
|
|
30
|
+
|
|
31
|
+
client = SecondOpinion(api_key="sk_2op_...")
|
|
32
|
+
|
|
33
|
+
# Get a code review
|
|
34
|
+
result = client.opinion(diff="your git diff here", llm="codex")
|
|
35
|
+
print(result["analysis"]["summary"])
|
|
36
|
+
|
|
37
|
+
# Ask a question
|
|
38
|
+
answer = client.ask(question="Is this SQL query safe?", code="SELECT * FROM users WHERE id = " + user_id)
|
|
39
|
+
print(answer["response"])
|
|
40
|
+
|
|
41
|
+
# Check usage
|
|
42
|
+
status = client.status()
|
|
43
|
+
print(f"Credits: {status['totalAvailable']}")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Available Methods
|
|
47
|
+
|
|
48
|
+
| Method | Description | Credits |
|
|
49
|
+
|--------|-------------|---------|
|
|
50
|
+
| `opinion(diff, llm, context)` | Single-model code review | 1 |
|
|
51
|
+
| `ask(question, llm, code, context)` | Ask an AI model a question | 1 |
|
|
52
|
+
| `review(diff, llm, context)` | Code review (alias for opinion) | 1 |
|
|
53
|
+
| `explain(diff, audience)` | Explain changes | 1 |
|
|
54
|
+
| `consensus(diff, context)` | 3-model consensus review | 3 |
|
|
55
|
+
| `bug_hunt(diff, context)` | 3-model bug detection | 3 |
|
|
56
|
+
| `security_audit(diff, context)` | OWASP security scan | 1-3 |
|
|
57
|
+
| `generate_tests(diff, llm, framework)` | Generate tests | 1 |
|
|
58
|
+
| `status()` | Check account credits | 0 |
|
|
59
|
+
|
|
60
|
+
## Links
|
|
61
|
+
|
|
62
|
+
- [Documentation](https://get2ndopinion.dev/docs)
|
|
63
|
+
- [Pricing](https://get2ndopinion.dev/pricing)
|
|
64
|
+
- [API Reference](https://get2ndopinion.dev/docs/api)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# secondopinion
|
|
2
|
+
|
|
3
|
+
Python SDK for the [2ndOpinion](https://get2ndopinion.dev) API — AI-to-AI code review.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install secondopinion-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from secondopinion import SecondOpinion
|
|
15
|
+
|
|
16
|
+
client = SecondOpinion(api_key="sk_2op_...")
|
|
17
|
+
|
|
18
|
+
# Get a code review
|
|
19
|
+
result = client.opinion(diff="your git diff here", llm="codex")
|
|
20
|
+
print(result["analysis"]["summary"])
|
|
21
|
+
|
|
22
|
+
# Ask a question
|
|
23
|
+
answer = client.ask(question="Is this SQL query safe?", code="SELECT * FROM users WHERE id = " + user_id)
|
|
24
|
+
print(answer["response"])
|
|
25
|
+
|
|
26
|
+
# Check usage
|
|
27
|
+
status = client.status()
|
|
28
|
+
print(f"Credits: {status['totalAvailable']}")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Available Methods
|
|
32
|
+
|
|
33
|
+
| Method | Description | Credits |
|
|
34
|
+
|--------|-------------|---------|
|
|
35
|
+
| `opinion(diff, llm, context)` | Single-model code review | 1 |
|
|
36
|
+
| `ask(question, llm, code, context)` | Ask an AI model a question | 1 |
|
|
37
|
+
| `review(diff, llm, context)` | Code review (alias for opinion) | 1 |
|
|
38
|
+
| `explain(diff, audience)` | Explain changes | 1 |
|
|
39
|
+
| `consensus(diff, context)` | 3-model consensus review | 3 |
|
|
40
|
+
| `bug_hunt(diff, context)` | 3-model bug detection | 3 |
|
|
41
|
+
| `security_audit(diff, context)` | OWASP security scan | 1-3 |
|
|
42
|
+
| `generate_tests(diff, llm, framework)` | Generate tests | 1 |
|
|
43
|
+
| `status()` | Check account credits | 0 |
|
|
44
|
+
|
|
45
|
+
## Links
|
|
46
|
+
|
|
47
|
+
- [Documentation](https://get2ndopinion.dev/docs)
|
|
48
|
+
- [Pricing](https://get2ndopinion.dev/pricing)
|
|
49
|
+
- [API Reference](https://get2ndopinion.dev/docs/api)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "secondopinion-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the 2ndOpinion API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "2ndOpinion", email = "hello@get2ndopinion.dev"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = ["httpx>=0.24.0"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://get2ndopinion.dev"
|
|
22
|
+
Repository = "https://github.com/brianmello8/2ndopinion"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["secondopinion"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""2ndOpinion Python SDK"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, AsyncIterator, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SecondOpinionError(Exception):
|
|
12
|
+
"""Error from the 2ndOpinion API."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message: str, status: int, code: str):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.status = status
|
|
17
|
+
self.code = code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SecondOpinion:
|
|
21
|
+
"""Client for the 2ndOpinion API.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
client = SecondOpinion(api_key="sk_2op_...")
|
|
26
|
+
result = client.opinion(diff="diff --git a/foo.ts ...")
|
|
27
|
+
print(result["analysis"]["summary"])
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str,
|
|
33
|
+
base_url: str = "https://get2ndopinion.dev",
|
|
34
|
+
timeout: float = 45.0,
|
|
35
|
+
max_retries: int = 2,
|
|
36
|
+
):
|
|
37
|
+
if not api_key:
|
|
38
|
+
raise ValueError("api_key is required")
|
|
39
|
+
self.api_key = api_key
|
|
40
|
+
self.base_url = base_url.rstrip("/")
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
self.max_retries = max_retries
|
|
43
|
+
self._client = httpx.Client(
|
|
44
|
+
base_url=self.base_url,
|
|
45
|
+
headers={
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"X-2ndOpinion-Key": self.api_key,
|
|
48
|
+
},
|
|
49
|
+
timeout=self.timeout,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
|
53
|
+
last_error: Optional[Exception] = None
|
|
54
|
+
|
|
55
|
+
for attempt in range(self.max_retries + 1):
|
|
56
|
+
try:
|
|
57
|
+
response = self._client.request(method, path, **kwargs)
|
|
58
|
+
|
|
59
|
+
if response.status_code >= 400:
|
|
60
|
+
body = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
|
61
|
+
error = SecondOpinionError(
|
|
62
|
+
body.get("error", f"HTTP {response.status_code}"),
|
|
63
|
+
response.status_code,
|
|
64
|
+
"RATE_LIMIT" if response.status_code == 429
|
|
65
|
+
else "INSUFFICIENT_CREDITS" if response.status_code == 402
|
|
66
|
+
else "FORBIDDEN" if response.status_code == 403
|
|
67
|
+
else "UNAUTHORIZED" if response.status_code == 401
|
|
68
|
+
else "API_ERROR",
|
|
69
|
+
)
|
|
70
|
+
if response.status_code != 429 and response.status_code < 500:
|
|
71
|
+
raise error
|
|
72
|
+
last_error = error
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
return response.json()
|
|
76
|
+
|
|
77
|
+
except SecondOpinionError:
|
|
78
|
+
raise
|
|
79
|
+
except Exception as e:
|
|
80
|
+
last_error = e
|
|
81
|
+
if attempt < self.max_retries:
|
|
82
|
+
time.sleep(2**attempt)
|
|
83
|
+
|
|
84
|
+
raise last_error or Exception("Request failed")
|
|
85
|
+
|
|
86
|
+
def opinion(
|
|
87
|
+
self,
|
|
88
|
+
diff: str,
|
|
89
|
+
context: str = "",
|
|
90
|
+
llm: str = "codex",
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Analyze a git diff for risks and suggestions."""
|
|
93
|
+
return self._request(
|
|
94
|
+
"POST",
|
|
95
|
+
"/api/gateway/opinion",
|
|
96
|
+
json={"diff": diff, "context": context, "llm": llm},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def review(
|
|
100
|
+
self,
|
|
101
|
+
diff: str,
|
|
102
|
+
context: str = "",
|
|
103
|
+
llm: str = "codex",
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""Detailed code review of a git diff."""
|
|
106
|
+
return self._request(
|
|
107
|
+
"POST",
|
|
108
|
+
"/api/gateway/review",
|
|
109
|
+
json={"diff": diff, "context": context, "llm": llm},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def ask(
|
|
113
|
+
self,
|
|
114
|
+
question: str,
|
|
115
|
+
context: str = "",
|
|
116
|
+
code: str = "",
|
|
117
|
+
llm: str = "codex",
|
|
118
|
+
) -> dict:
|
|
119
|
+
"""Ask a question about code."""
|
|
120
|
+
return self._request(
|
|
121
|
+
"POST",
|
|
122
|
+
"/api/gateway/ask",
|
|
123
|
+
json={"question": question, "context": context, "code": code, "llm": llm},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def batch(
|
|
127
|
+
self,
|
|
128
|
+
files: list[dict],
|
|
129
|
+
llm: str = "codex",
|
|
130
|
+
) -> dict:
|
|
131
|
+
"""Analyze multiple files in one request.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
files: List of {"file": "path", "diff": "diff content"}
|
|
135
|
+
llm: LLM to use
|
|
136
|
+
"""
|
|
137
|
+
return self._request(
|
|
138
|
+
"POST",
|
|
139
|
+
"/api/gateway/batch",
|
|
140
|
+
json={"files": files, "llm": llm},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def status(self) -> dict:
|
|
144
|
+
"""Get account status, usage, and available features."""
|
|
145
|
+
return self._request("GET", "/api/gateway/status")
|
|
146
|
+
|
|
147
|
+
def stream(
|
|
148
|
+
self,
|
|
149
|
+
diff: str,
|
|
150
|
+
context: str = "",
|
|
151
|
+
llm: str = "codex",
|
|
152
|
+
) -> httpx.Response:
|
|
153
|
+
"""Stream an analysis response (SSE).
|
|
154
|
+
|
|
155
|
+
Returns an httpx Response that can be iterated::
|
|
156
|
+
|
|
157
|
+
with client.stream(diff="...") as response:
|
|
158
|
+
for line in response.iter_lines():
|
|
159
|
+
if line.startswith("data: "):
|
|
160
|
+
print(line[6:])
|
|
161
|
+
"""
|
|
162
|
+
return self._client.stream(
|
|
163
|
+
"POST",
|
|
164
|
+
"/api/gateway/opinion/stream",
|
|
165
|
+
json={"diff": diff, "context": context, "llm": llm},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def close(self):
|
|
169
|
+
"""Close the HTTP client."""
|
|
170
|
+
self._client.close()
|
|
171
|
+
|
|
172
|
+
def __enter__(self):
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __exit__(self, *args):
|
|
176
|
+
self.close()
|