sysmlv2copilot 0.2.0__py3-none-any.whl

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,49 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .client import SysMLCopilot
4
+ from .exceptions import (
5
+ APIConnectionError,
6
+ APIResponseValidationError,
7
+ APIStatusError,
8
+ AuthenticationError,
9
+ RateLimitError,
10
+ SysMLCopilotError,
11
+ )
12
+ from .types import (
13
+ ArtifactReference,
14
+ DependencyStatus,
15
+ ErrorPayload,
16
+ HealthResponse,
17
+ RepairCreateParams,
18
+ RepairObject,
19
+ RequestDetail,
20
+ ResponseCreateParams,
21
+ ResponseMetadata,
22
+ ResponseObject,
23
+ )
24
+
25
+ try:
26
+ __version__ = version("sysmlv2copilot")
27
+ except PackageNotFoundError:
28
+ __version__ = "0.2.0"
29
+
30
+ __all__ = [
31
+ "APIConnectionError",
32
+ "APIResponseValidationError",
33
+ "APIStatusError",
34
+ "ArtifactReference",
35
+ "AuthenticationError",
36
+ "DependencyStatus",
37
+ "ErrorPayload",
38
+ "HealthResponse",
39
+ "RateLimitError",
40
+ "RepairCreateParams",
41
+ "RepairObject",
42
+ "RequestDetail",
43
+ "ResponseCreateParams",
44
+ "ResponseMetadata",
45
+ "ResponseObject",
46
+ "SysMLCopilot",
47
+ "SysMLCopilotError",
48
+ "__version__",
49
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
sysmlv2copilot/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from . import __version__
10
+ from .client import SysMLCopilot
11
+ from .exceptions import SysMLCopilotError
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(
16
+ prog="sysmlv2copilot",
17
+ description="CLI client for the SysML refinement API.",
18
+ )
19
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
20
+ parser.add_argument(
21
+ "--base-url",
22
+ default=os.getenv("SYSMLV2COPILOT_BASE_URL", "http://127.0.0.1:8000"),
23
+ help="API base URL.",
24
+ )
25
+ parser.add_argument(
26
+ "--api-key",
27
+ default=os.getenv("SYSMLV2COPILOT_API_KEY"),
28
+ help="Bearer API key. Defaults to SYSMLV2COPILOT_API_KEY.",
29
+ )
30
+
31
+ subparsers = parser.add_subparsers(dest="command", required=True)
32
+
33
+ responses_parser = subparsers.add_parser("responses", help="Create a refinement response.")
34
+ responses_subparsers = responses_parser.add_subparsers(dest="responses_command", required=True)
35
+ create_parser = responses_subparsers.add_parser("create", help="Create a response.")
36
+ create_group = create_parser.add_mutually_exclusive_group(required=True)
37
+ create_group.add_argument("--input", help="Inline natural-language input text.")
38
+ create_group.add_argument("--input-file", type=Path, help="Path to a file containing the input.")
39
+ create_parser.add_argument("--model", default="refine-sysml-v1")
40
+ create_parser.add_argument("--provider", choices=["openai", "anthropic"])
41
+ create_parser.add_argument("--upstream-model")
42
+ create_parser.add_argument("--max-iters", type=int, default=10)
43
+ create_parser.add_argument("--max-total-tokens", type=int, default=40_000)
44
+ create_parser.add_argument("--temperature", type=float)
45
+
46
+ repairs_parser = subparsers.add_parser("repairs", help="Repair existing SysML.")
47
+ repairs_subparsers = repairs_parser.add_subparsers(dest="repairs_command", required=True)
48
+ repair_create_parser = repairs_subparsers.add_parser("create", help="Repair a SysML document.")
49
+ repair_group = repair_create_parser.add_mutually_exclusive_group(required=True)
50
+ repair_group.add_argument("--input", help="Inline SysML input text.")
51
+ repair_group.add_argument("--input-file", type=Path, help="Path to a file containing SysML.")
52
+ repair_create_parser.add_argument("--model", default="refine-sysml-v1")
53
+ repair_create_parser.add_argument("--provider", choices=["openai", "anthropic"])
54
+ repair_create_parser.add_argument("--upstream-model")
55
+ repair_create_parser.add_argument("--max-iters", type=int, default=10)
56
+ repair_create_parser.add_argument("--max-total-tokens", type=int, default=40_000)
57
+ repair_create_parser.add_argument("--temperature", type=float)
58
+
59
+ requests_parser = subparsers.add_parser("requests", help="Fetch a stored request.")
60
+ requests_subparsers = requests_parser.add_subparsers(dest="requests_command", required=True)
61
+ retrieve_parser = requests_subparsers.add_parser("retrieve", help="Retrieve a request by id.")
62
+ retrieve_parser.add_argument("request_id")
63
+
64
+ health_parser = subparsers.add_parser("health", help="Check service health.")
65
+ health_subparsers = health_parser.add_subparsers(dest="health_command", required=True)
66
+ health_subparsers.add_parser("check", help="Get /healthz.")
67
+ return parser
68
+
69
+
70
+ def _read_input_text(args: argparse.Namespace) -> str:
71
+ if args.input is not None:
72
+ return args.input
73
+ return args.input_file.read_text(encoding="utf-8")
74
+
75
+
76
+ def _print_json(data: Any) -> None:
77
+ print(json.dumps(data, indent=2, sort_keys=True))
78
+
79
+
80
+ def main(argv: list[str] | None = None) -> int:
81
+ parser = build_parser()
82
+ args = parser.parse_args(argv)
83
+ try:
84
+ with SysMLCopilot(api_key=args.api_key, base_url=args.base_url) as client:
85
+ if args.command == "responses" and args.responses_command == "create":
86
+ response = client.responses.create(
87
+ input=_read_input_text(args),
88
+ model=args.model,
89
+ provider=args.provider,
90
+ upstream_model=args.upstream_model,
91
+ max_iters=args.max_iters,
92
+ max_total_tokens=args.max_total_tokens,
93
+ temperature=args.temperature,
94
+ )
95
+ _print_json(response.model_dump())
96
+ return 0
97
+ if args.command == "repairs" and args.repairs_command == "create":
98
+ response = client.repairs.create(
99
+ input=_read_input_text(args),
100
+ model=args.model,
101
+ provider=args.provider,
102
+ upstream_model=args.upstream_model,
103
+ max_iters=args.max_iters,
104
+ max_total_tokens=args.max_total_tokens,
105
+ temperature=args.temperature,
106
+ )
107
+ _print_json(response.model_dump())
108
+ return 0
109
+ if args.command == "requests" and args.requests_command == "retrieve":
110
+ detail = client.requests.retrieve(args.request_id)
111
+ _print_json(detail.model_dump())
112
+ return 0
113
+ if args.command == "health" and args.health_command == "check":
114
+ health = client.health.check()
115
+ _print_json(health.model_dump())
116
+ return 0
117
+ except SysMLCopilotError as exc:
118
+ print(f"error: {exc}")
119
+ return 1
120
+ parser.error("Unsupported command.")
121
+ return 2
@@ -0,0 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import httpx
7
+ from pydantic import ValidationError
8
+
9
+ from .exceptions import (
10
+ APIConnectionError,
11
+ APIResponseValidationError,
12
+ APIStatusError,
13
+ AuthenticationError,
14
+ RateLimitError,
15
+ )
16
+ from .types import (
17
+ HealthResponse,
18
+ RepairCreateParams,
19
+ RepairObject,
20
+ RequestDetail,
21
+ ResponseCreateParams,
22
+ ResponseObject,
23
+ )
24
+
25
+
26
+ def _normalize_base_url(base_url: str) -> str:
27
+ return base_url.rstrip("/")
28
+
29
+
30
+ class _BaseResource:
31
+ def __init__(self, client: "SysMLCopilot") -> None:
32
+ self._client = client
33
+
34
+ def _request(self, method: str, path: str, *, json_body: Mapping[str, Any] | None = None) -> Any:
35
+ return self._client._request(method, path, json_body=json_body)
36
+
37
+
38
+ class ResponsesResource(_BaseResource):
39
+ def create(
40
+ self,
41
+ *,
42
+ input: str,
43
+ model: str = "refine-sysml-v1",
44
+ provider: str | None = None,
45
+ upstream_model: str | None = None,
46
+ max_iters: int = 10,
47
+ max_total_tokens: int = 40_000,
48
+ temperature: float | None = None,
49
+ ) -> ResponseObject:
50
+ payload = ResponseCreateParams(
51
+ input=input,
52
+ model=model,
53
+ provider=provider,
54
+ upstream_model=upstream_model,
55
+ max_iters=max_iters,
56
+ max_total_tokens=max_total_tokens,
57
+ temperature=temperature,
58
+ )
59
+ data = self._request(
60
+ "POST",
61
+ "/v1/responses",
62
+ json_body=payload.model_dump(exclude_none=True),
63
+ )
64
+ try:
65
+ return ResponseObject.model_validate(data)
66
+ except ValidationError as exc:
67
+ raise APIResponseValidationError(f"Invalid response payload: {exc}") from exc
68
+
69
+
70
+ class RequestsResource(_BaseResource):
71
+ def retrieve(self, request_id: str) -> RequestDetail:
72
+ data = self._request("GET", f"/v1/requests/{request_id}")
73
+ try:
74
+ return RequestDetail.model_validate(data)
75
+ except ValidationError as exc:
76
+ raise APIResponseValidationError(f"Invalid request detail payload: {exc}") from exc
77
+
78
+
79
+ class RepairsResource(_BaseResource):
80
+ def create(
81
+ self,
82
+ *,
83
+ input: str,
84
+ model: str = "refine-sysml-v1",
85
+ provider: str | None = None,
86
+ upstream_model: str | None = None,
87
+ max_iters: int = 10,
88
+ max_total_tokens: int = 40_000,
89
+ temperature: float | None = None,
90
+ ) -> RepairObject:
91
+ payload = RepairCreateParams(
92
+ input=input,
93
+ model=model,
94
+ provider=provider,
95
+ upstream_model=upstream_model,
96
+ max_iters=max_iters,
97
+ max_total_tokens=max_total_tokens,
98
+ temperature=temperature,
99
+ )
100
+ data = self._request(
101
+ "POST",
102
+ "/v1/repairs",
103
+ json_body=payload.model_dump(exclude_none=True),
104
+ )
105
+ try:
106
+ return RepairObject.model_validate(data)
107
+ except ValidationError as exc:
108
+ raise APIResponseValidationError(f"Invalid repair payload: {exc}") from exc
109
+
110
+
111
+ class HealthResource(_BaseResource):
112
+ def check(self) -> HealthResponse:
113
+ data = self._request("GET", "/healthz")
114
+ try:
115
+ return HealthResponse.model_validate(data)
116
+ except ValidationError as exc:
117
+ raise APIResponseValidationError(f"Invalid health payload: {exc}") from exc
118
+
119
+
120
+ class SysMLCopilot:
121
+ def __init__(
122
+ self,
123
+ *,
124
+ api_key: str | None = None,
125
+ base_url: str = "http://127.0.0.1:8000",
126
+ timeout: float = 120.0,
127
+ http_client: httpx.Client | None = None,
128
+ default_headers: Mapping[str, str] | None = None,
129
+ ) -> None:
130
+ self.base_url = _normalize_base_url(base_url)
131
+ self._owns_client = http_client is None
132
+ headers = {"Accept": "application/json"}
133
+ if default_headers:
134
+ headers.update(default_headers)
135
+ if api_key:
136
+ headers["Authorization"] = f"Bearer {api_key}"
137
+ if http_client is None:
138
+ self._client = httpx.Client(
139
+ base_url=self.base_url,
140
+ headers=headers,
141
+ timeout=timeout,
142
+ )
143
+ else:
144
+ http_client.headers.update(headers)
145
+ self._client = http_client
146
+ self.responses = ResponsesResource(self)
147
+ self.repairs = RepairsResource(self)
148
+ self.requests = RequestsResource(self)
149
+ self.health = HealthResource(self)
150
+
151
+ def close(self) -> None:
152
+ if self._owns_client:
153
+ self._client.close()
154
+
155
+ def __enter__(self) -> "SysMLCopilot":
156
+ return self
157
+
158
+ def __exit__(self, exc_type, exc, tb) -> None:
159
+ self.close()
160
+
161
+ def _request(self, method: str, path: str, *, json_body: Mapping[str, Any] | None = None) -> Any:
162
+ try:
163
+ response = self._client.request(method, path, json=json_body)
164
+ except httpx.HTTPError as exc:
165
+ raise APIConnectionError(f"Request failed: {exc}") from exc
166
+ if response.is_error:
167
+ self._raise_for_status(response)
168
+ try:
169
+ return response.json()
170
+ except ValueError as exc:
171
+ raise APIResponseValidationError(f"Response was not valid JSON: {exc}") from exc
172
+
173
+ def _raise_for_status(self, response: httpx.Response) -> None:
174
+ body: Any
175
+ try:
176
+ body = response.json()
177
+ except ValueError:
178
+ body = response.text
179
+
180
+ detail = body.get("detail") if isinstance(body, dict) else None
181
+ payload = detail.get("error") if isinstance(detail, dict) else None
182
+ error_type = payload.get("type") if isinstance(payload, dict) else None
183
+ message = payload.get("message") if isinstance(payload, dict) else response.text
184
+
185
+ exc_cls: type[APIStatusError]
186
+ if response.status_code == 401:
187
+ exc_cls = AuthenticationError
188
+ elif response.status_code == 429:
189
+ exc_cls = RateLimitError
190
+ else:
191
+ exc_cls = APIStatusError
192
+ raise exc_cls(
193
+ message or f"API request failed with status {response.status_code}",
194
+ status_code=response.status_code,
195
+ body=body,
196
+ error_type=error_type,
197
+ )
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class SysMLCopilotError(Exception):
7
+ """Base SDK exception."""
8
+
9
+
10
+ class APIConnectionError(SysMLCopilotError):
11
+ """Raised when the API cannot be reached."""
12
+
13
+
14
+ class APIResponseValidationError(SysMLCopilotError):
15
+ """Raised when the API response shape is invalid."""
16
+
17
+
18
+ class APIStatusError(SysMLCopilotError):
19
+ """Raised for non-2xx API responses."""
20
+
21
+ def __init__(
22
+ self,
23
+ message: str,
24
+ *,
25
+ status_code: int,
26
+ body: Any | None = None,
27
+ error_type: str | None = None,
28
+ ) -> None:
29
+ super().__init__(message)
30
+ self.status_code = status_code
31
+ self.body = body
32
+ self.error_type = error_type
33
+
34
+
35
+ class AuthenticationError(APIStatusError):
36
+ """Raised for 401 responses."""
37
+
38
+
39
+ class RateLimitError(APIStatusError):
40
+ """Raised for 429 responses."""
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ResponseCreateParams(BaseModel):
9
+ input: str
10
+ model: str = "refine-sysml-v1"
11
+ provider: Literal["openai", "anthropic"] | None = None
12
+ upstream_model: str | None = None
13
+ max_iters: int = Field(default=10, ge=1, le=50)
14
+ max_total_tokens: int = Field(default=40_000, ge=1, le=250_000)
15
+ temperature: float | None = Field(default=None, ge=0, le=2)
16
+
17
+
18
+ class RepairCreateParams(ResponseCreateParams):
19
+ pass
20
+
21
+
22
+ class ErrorPayload(BaseModel):
23
+ type: str
24
+ message: str
25
+
26
+
27
+ class ArtifactReference(BaseModel):
28
+ id: str
29
+ artifact_type: str
30
+ storage_backend: str
31
+ storage_path: str
32
+ content_type: str
33
+ size_bytes: int
34
+ created_at: str
35
+
36
+
37
+ class ResponseMetadata(BaseModel):
38
+ operation: Literal["generate", "repair"] | None = None
39
+ provider: Literal["openai", "anthropic"] | None = None
40
+ upstream_model: str | None = None
41
+ run_id: str | None = None
42
+ diagnostics_summary: str | None = None
43
+ initial_compiler_passed: bool | None = None
44
+ artifact_paths: dict[str, str] = Field(default_factory=dict)
45
+ usage: dict[str, int | None] = Field(default_factory=dict)
46
+
47
+
48
+ class ResponseObject(BaseModel):
49
+ id: str
50
+ object: Literal["response"] = "response"
51
+ status: Literal["completed", "failed"]
52
+ model: str
53
+ output_text: str | None = None
54
+ compiler_passed: bool | None = None
55
+ iterations_completed: int | None = None
56
+ started_at: str
57
+ finished_at: str | None = None
58
+ duration_ms: int | None = None
59
+ request_user_id: str
60
+ request_user_email: str | None = None
61
+ request_user_name: str | None = None
62
+ metadata: ResponseMetadata
63
+ error: ErrorPayload | None = None
64
+
65
+
66
+ class RepairObject(BaseModel):
67
+ id: str
68
+ object: Literal["repair"] = "repair"
69
+ status: Literal["completed", "failed"]
70
+ model: str
71
+ output_text: str | None = None
72
+ compiler_passed: bool | None = None
73
+ iterations_completed: int | None = None
74
+ started_at: str
75
+ finished_at: str | None = None
76
+ duration_ms: int | None = None
77
+ request_user_id: str
78
+ request_user_email: str | None = None
79
+ request_user_name: str | None = None
80
+ metadata: ResponseMetadata
81
+ error: ErrorPayload | None = None
82
+
83
+
84
+ class RequestDetail(BaseModel):
85
+ id: str
86
+ object: Literal["request"] = "request"
87
+ status: str
88
+ model: str
89
+ output_text: str | None = None
90
+ compiler_passed: bool | None = None
91
+ iterations_completed: int | None = None
92
+ started_at: str
93
+ finished_at: str | None = None
94
+ duration_ms: int | None = None
95
+ request_user_id: str
96
+ request_user_email: str | None = None
97
+ request_user_name: str | None = None
98
+ metadata: ResponseMetadata
99
+ error: ErrorPayload | None = None
100
+ artifacts: list[ArtifactReference] = Field(default_factory=list)
101
+
102
+
103
+ class DependencyStatus(BaseModel):
104
+ available: bool
105
+ detail: str | None = None
106
+
107
+
108
+ class HealthResponse(BaseModel):
109
+ status: Literal["ok"] = "ok"
110
+ service: str
111
+ version: str
112
+ dependencies: dict[str, DependencyStatus]
@@ -0,0 +1,432 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysmlv2copilot
3
+ Version: 0.2.0
4
+ Summary: Python client SDK and CLI for the SysML refinement API
5
+ Author: Chance LaVoie
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cmuchancel/sysmlv2copilot
8
+ Project-URL: Repository, https://github.com/cmuchancel/sysmlv2copilot
9
+ Project-URL: Issues, https://github.com/cmuchancel/sysmlv2copilot/issues
10
+ Project-URL: Documentation, https://github.com/cmuchancel/sysmlv2copilot/tree/main/docs
11
+ Keywords: sysml,sysmlv2,api-client,sdk,llm
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: httpx<1,>=0.28
22
+ Requires-Dist: pydantic<3,>=2.7
23
+ Provides-Extra: server
24
+ Requires-Dist: alembic<2,>=1.14; extra == "server"
25
+ Requires-Dist: anthropic<1,>=0.84; extra == "server"
26
+ Requires-Dist: fastapi<1,>=0.116; extra == "server"
27
+ Requires-Dist: openai<2,>=1.0; extra == "server"
28
+ Requires-Dist: psycopg[binary]<4,>=3.2; extra == "server"
29
+ Requires-Dist: pydantic-settings<3,>=2.7; extra == "server"
30
+ Requires-Dist: python-dotenv<2,>=1.0; extra == "server"
31
+ Requires-Dist: sqlalchemy<3,>=2.0; extra == "server"
32
+ Requires-Dist: uvicorn<1,>=0.35; extra == "server"
33
+ Provides-Extra: admin
34
+ Requires-Dist: pandas<3,>=2.2; extra == "admin"
35
+ Requires-Dist: streamlit<2,>=1.45; extra == "admin"
36
+ Provides-Extra: dev
37
+ Requires-Dist: build<2,>=1.2; extra == "dev"
38
+ Requires-Dist: pytest<9,>=8.3; extra == "dev"
39
+ Requires-Dist: twine<7,>=5; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # SysML Refinement API
43
+
44
+ This repository contains two distinct products:
45
+ - a public Python SDK and CLI, published as `sysmlv2copilot`
46
+ - an internal server/admin implementation that stays hosted and black-box
47
+
48
+ ## Repo Split
49
+
50
+ The repository is now organized into two explicit halves:
51
+ - `api/`: index for the service/SDK/admin side
52
+ - `finetune/`: local dataset generation, repair corpus, and fine-tuning assets
53
+
54
+ To avoid breaking runtime imports in one large move, the API code still currently lives in the root-owned paths such as `app/`, `alembic/`, `sysmlv2copilot/`, `sysmlv2copilot_admin/`, and the main `scripts/` directory. The fine-tuning assets now live directly under `finetune/`.
55
+
56
+ The hosted service accepts either natural-language requirements or SysML input, runs the compiler-in-the-loop workflow, returns SysML plus structured metadata, and stores each request with a single persisted artifact bundle.
57
+
58
+ The service itself is intentionally narrow:
59
+ - internal-only
60
+ - one user, one API key
61
+ - plain text in, SysML text out
62
+ - FastAPI HTTP layer
63
+ - Postgres-compatible metadata storage
64
+ - filesystem artifact storage by default
65
+ - admin operations through CLI scripts, not public admin routes
66
+
67
+ ## Public SDK
68
+
69
+ The public package is client-only. It does not ship the backend, migrations, deployment code, or admin console.
70
+
71
+ Install from PyPI:
72
+
73
+ ```bash
74
+ python -m pip install sysmlv2copilot
75
+ ```
76
+
77
+ Basic generation:
78
+
79
+ ```python
80
+ from sysmlv2copilot import SysMLCopilot
81
+
82
+ with SysMLCopilot(
83
+ api_key="sysml_live_...",
84
+ base_url="https://your-deployment.example.com",
85
+ ) as client:
86
+ response = client.responses.create(
87
+ input="Design a compact warehouse inspection drone that can hover for 15 minutes.",
88
+ provider="openai",
89
+ )
90
+ print(response.id)
91
+ print(response.output_text)
92
+ ```
93
+
94
+ Basic repair:
95
+
96
+ ```python
97
+ from sysmlv2copilot import SysMLCopilot
98
+
99
+ broken_sysml = """package Example {
100
+ public import ScalarValues::*;
101
+ requirement def R { text = "Missing semicolon" }
102
+ }
103
+ """
104
+
105
+ with SysMLCopilot(
106
+ api_key="sysml_live_...",
107
+ base_url="https://your-deployment.example.com",
108
+ ) as client:
109
+ repaired = client.repairs.create(input=broken_sysml, provider="openai")
110
+ print(repaired.compiler_passed)
111
+ print(repaired.output_text)
112
+ ```
113
+
114
+ CLI examples:
115
+
116
+ ```bash
117
+ sysmlv2copilot --api-key sysml_live_... \
118
+ --base-url https://your-deployment.example.com \
119
+ responses create \
120
+ --input "Design a compact warehouse inspection drone that can hover for 15 minutes."
121
+
122
+ sysmlv2copilot --api-key sysml_live_... \
123
+ --base-url https://your-deployment.example.com \
124
+ repairs create \
125
+ --input-file ./broken.sysml
126
+ ```
127
+
128
+ SDK details:
129
+ - [docs/PYTHON_SDK.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/PYTHON_SDK.md)
130
+ - [docs/PYPI_RELEASE.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/PYPI_RELEASE.md)
131
+
132
+ ## What It Solves
133
+
134
+ The upstream pipeline already knows how to:
135
+ - build the iterative refinement prompt
136
+ - call the OpenAI Responses API
137
+ - write every iteration prompt and candidate SysML file
138
+ - run `syside check`
139
+ - capture compiler diagnostics
140
+ - stop on success or refinement limits
141
+
142
+ This service wraps that exact behavior in an authenticated API with request tracking and persistent artifacts so an internal team can use it immediately.
143
+
144
+ The refinement loop now supports either upstream provider:
145
+ - OpenAI via the Responses API
146
+ - Anthropic via the Messages API
147
+
148
+ The public API surface stays narrow and OpenAI-style. Provider choice is an internal runtime option exposed as an extra request field.
149
+
150
+ ## Repo Layout
151
+
152
+ - `app/`: FastAPI app, auth, DB models, storage, and upstream wrapper
153
+ - `app/upstream/refine_sysml.py`: vendored upstream refiner used as the behavior source of truth
154
+ - `context/upstream/`: tight context package copied from the upstream repo
155
+ - `scripts/`: admin CLI for user creation, API key reset, and usage reporting
156
+ - `alembic/`: schema migrations
157
+ - `sysmlv2copilot/`: public client SDK and CLI package
158
+ - `sysmlv2copilot_admin/`: internal Streamlit admin console
159
+ - `tests/`: API, CLI, and config coverage
160
+
161
+ ## Internal Repo Setup
162
+
163
+ If you are working on the hosted backend from this repo, install the internal extras:
164
+
165
+ 1. Create a virtual environment and install dependencies:
166
+
167
+ ```bash
168
+ python3 -m venv .venv
169
+ source .venv/bin/activate
170
+ python -m pip install --upgrade pip
171
+ python -m pip install -e ".[server,admin,dev]"
172
+ ```
173
+
174
+ 2. Create `.env`:
175
+
176
+ ```bash
177
+ cp .env.example .env
178
+ ```
179
+
180
+ 3. Fill in at least:
181
+ - one or both of `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`
182
+ - one of `SYSIDE_VENV_PATH` or `SYSIDE_EXECUTABLE_PATH`
183
+ - `API_KEY_HASH_PEPPER`
184
+ - optionally a PostgreSQL `DATABASE_URL`
185
+
186
+ SQLite works for local development. PostgreSQL or Supabase Postgres is preferred for shared environments.
187
+ For PostgreSQL or Supabase, install/runtime support now includes `psycopg`.
188
+
189
+ ## Migrations
190
+
191
+ Run the schema migration before starting the service:
192
+
193
+ ```bash
194
+ alembic upgrade head
195
+ ```
196
+
197
+ ## Start The Service
198
+
199
+ ```bash
200
+ uvicorn app.main:app --reload
201
+ ```
202
+
203
+ The default address is `http://127.0.0.1:8000`.
204
+
205
+ ## Create A User And API Key
206
+
207
+ Create a user and print the plaintext key once:
208
+
209
+ ```bash
210
+ python scripts/create_user.py --email engineer@example.com --name "Internal Engineer"
211
+ ```
212
+
213
+ Reset the single API key for an existing user:
214
+
215
+ ```bash
216
+ python scripts/reset_api_key.py --email engineer@example.com
217
+ ```
218
+
219
+ List usage:
220
+
221
+ ```bash
222
+ python scripts/list_usage.py --email engineer@example.com --from-date 2026-03-01 --to-date 2026-03-31
223
+ ```
224
+
225
+ ## Call The API
226
+
227
+ ```bash
228
+ curl -s http://127.0.0.1:8000/v1/responses \
229
+ -H "Authorization: Bearer sysml_live_..." \
230
+ -H "Content-Type: application/json" \
231
+ -d '{
232
+ "input": "Design a compact battery-powered inspection drone that fits in a 30 cm cube and flies for 20 minutes.",
233
+ "model": "refine-sysml-v1",
234
+ "provider": "openai",
235
+ "max_iters": 10,
236
+ "max_total_tokens": 40000,
237
+ "temperature": null
238
+ }'
239
+ ```
240
+
241
+ Example successful response:
242
+
243
+ ```json
244
+ {
245
+ "id": "resp_20260312_161200_abcd",
246
+ "object": "response",
247
+ "status": "completed",
248
+ "model": "refine-sysml-v1",
249
+ "output_text": "package RequirementsOnly { ... }",
250
+ "compiler_passed": true,
251
+ "iterations_completed": 4,
252
+ "started_at": "2026-03-12T16:12:00Z",
253
+ "finished_at": "2026-03-12T16:12:22Z",
254
+ "duration_ms": 22014,
255
+ "request_user_id": "usr_1234",
256
+ "metadata": {
257
+ "provider": "openai",
258
+ "upstream_model": "gpt-5-mini",
259
+ "run_id": "run_20260312_161200_abcd",
260
+ "artifact_paths": {
261
+ "request_artifact": "/abs/path/request_artifact.json"
262
+ }
263
+ },
264
+ "error": null
265
+ }
266
+ ```
267
+
268
+ Repair request example:
269
+
270
+ ```bash
271
+ curl -s http://127.0.0.1:8000/v1/repairs \
272
+ -H "Authorization: Bearer sysml_live_..." \
273
+ -H "Content-Type: application/json" \
274
+ -d '{
275
+ "input": "package Example { requirement def R { text = \"Missing semicolon\" } }",
276
+ "model": "refine-sysml-v1",
277
+ "provider": "openai"
278
+ }'
279
+ ```
280
+
281
+ Anthropic request example:
282
+
283
+ ```bash
284
+ curl -s http://127.0.0.1:8000/v1/responses \
285
+ -H "Authorization: Bearer sysml_live_..." \
286
+ -H "Content-Type: application/json" \
287
+ -d '{
288
+ "input": "Design a compact battery-powered inspection drone that fits in a 30 cm cube and flies for 20 minutes.",
289
+ "model": "refine-sysml-v1",
290
+ "provider": "anthropic",
291
+ "upstream_model": "claude-sonnet-4-5-20250929"
292
+ }'
293
+ ```
294
+
295
+ Check a stored request:
296
+
297
+ ```bash
298
+ curl -s http://127.0.0.1:8000/v1/requests/resp_20260312_161200_abcd \
299
+ -H "Authorization: Bearer sysml_live_..."
300
+ ```
301
+
302
+ Health check:
303
+
304
+ ```bash
305
+ curl -s http://127.0.0.1:8000/healthz
306
+ ```
307
+
308
+ ## Repair Workflow
309
+
310
+ The hosted API now supports a repair-oriented workflow:
311
+ - accept raw SysML text
312
+ - run SysIDE immediately
313
+ - if the model already passes, return it unchanged with `iterations_completed = 0`
314
+ - if it fails, feed the broken SysML plus real compiler diagnostics into the repair loop
315
+ - persist the same request tracking and single-artifact bundle used by generation requests
316
+
317
+ Repair metadata includes:
318
+ - `metadata.operation = "repair"`
319
+ - `metadata.initial_compiler_passed`
320
+ - `metadata.diagnostics_summary`
321
+
322
+ Design note:
323
+ - [docs/ITERATION_STALL_POLICY.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/ITERATION_STALL_POLICY.md)
324
+
325
+ ## Admin Frontend
326
+
327
+ Run the Streamlit admin console:
328
+
329
+ ```bash
330
+ streamlit run sysmlv2copilot_admin/app.py
331
+ ```
332
+
333
+ It uses the real configured database and supports:
334
+ - create users and issue API keys
335
+ - rotate existing keys
336
+ - inspect per-user usage totals
337
+ - inspect overall usage and compiler performance
338
+ - browse recent request activity
339
+ - check live API health against a configured base URL
340
+
341
+ More detail:
342
+ - [docs/ADMIN_FRONTEND.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/ADMIN_FRONTEND.md)
343
+ - [docs/PYPI_RELEASE.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/PYPI_RELEASE.md)
344
+
345
+ ## Artifact Storage
346
+
347
+ Artifacts are stored under `ARTIFACT_ROOT`, defaulting to `./data/artifacts`. Each request gets a run directory with a single persisted artifact:
348
+ - `request_artifact.json`
349
+
350
+ That JSON bundle stores:
351
+ - the original input text
352
+ - the final SysML output
353
+ - request statistics such as tokens, chars, duration, and iterations
354
+ - provider/model metadata and any terminal error
355
+
356
+ Each request registers exactly one row in the `artifacts` table. Temporary iteration files used during SysIDE checks are removed before persistence.
357
+
358
+ ## Tests
359
+
360
+ Run the test suite with:
361
+
362
+ ```bash
363
+ pytest
364
+ ```
365
+
366
+ The tests mock the expensive upstream generation path and cover auth, request tracking, repair flow, artifacts, CLI operations, health reporting, packaging boundaries, and config validation.
367
+
368
+ ## Docker
369
+
370
+ Build:
371
+
372
+ ```bash
373
+ docker build \
374
+ --build-arg SYSIDE_PIP_SPEC='syside==<your-version>' \
375
+ --build-arg REQUIRE_SYSIDE=1 \
376
+ -t sysml-refinement-api .
377
+ ```
378
+
379
+ Run:
380
+
381
+ ```bash
382
+ docker run --rm -p 8000:8000 --env-file .env sysml-refinement-api
383
+ ```
384
+
385
+ The container runs `alembic upgrade head` before starting `uvicorn`.
386
+
387
+ ## Azure And Supabase
388
+
389
+ The repo is now prepared for:
390
+ - Azure Container Apps for compute
391
+ - Azure Container Registry for the private image
392
+ - Azure Files for persistent artifacts
393
+ - Supabase Postgres for metadata
394
+
395
+ Relevant files:
396
+ - [docs/AZURE_CONTAINER_APPS.md](/Users/chancelavoie/Desktop/sysmlv2copilot/docs/AZURE_CONTAINER_APPS.md)
397
+ - [scripts/deploy_azure_container_app.sh](/Users/chancelavoie/Desktop/sysmlv2copilot/scripts/deploy_azure_container_app.sh)
398
+ - [scripts/build_database_url.py](/Users/chancelavoie/Desktop/sysmlv2copilot/scripts/build_database_url.py)
399
+ - [scripts/render_azure_containerapp_yaml.py](/Users/chancelavoie/Desktop/sysmlv2copilot/scripts/render_azure_containerapp_yaml.py)
400
+ - [infra/supabase/create_service_role.sql](/Users/chancelavoie/Desktop/sysmlv2copilot/infra/supabase/create_service_role.sql)
401
+
402
+ To build a Supabase-compatible SQLAlchemy URL:
403
+
404
+ ```bash
405
+ python scripts/build_database_url.py \
406
+ --host db.<project-ref>.supabase.co \
407
+ --port 5432 \
408
+ --database postgres \
409
+ --user sysml_api_service \
410
+ --password 'replace-me'
411
+ ```
412
+
413
+ To deploy to Azure Container Apps:
414
+
415
+ ```bash
416
+ export BUILD_MODE=local-docker
417
+ bash scripts/deploy_azure_container_app.sh
418
+ ```
419
+
420
+ ## Upstream Context
421
+
422
+ The copied upstream context package lives in:
423
+ - `context/upstream/refine_sysml.py`
424
+ - `context/upstream/setup_pipeline_env.sh`
425
+ - `context/upstream/pipeline_README.md`
426
+ - `context/upstream/pipeline_HELP.md`
427
+ - `context/upstream/env.example`
428
+ - `context/examples/example_prompt.txt`
429
+ - `context/examples/example_output.sysml`
430
+ - `context/examples/example_run_log.json`
431
+
432
+ That context is intentionally tight and keeps the original refinement flow visible without copying the whole older repository.
@@ -0,0 +1,12 @@
1
+ sysmlv2copilot/__init__.py,sha256=p2Mh-FKAw1Kttgd5uzR-mVXziyrfrS3nB2q6at6GFcQ,1049
2
+ sysmlv2copilot/__main__.py,sha256=PSQ4rpL0dG6f-qH4N7H-gD9igQkdHzH4yVZDcW8lfZo,80
3
+ sysmlv2copilot/cli.py,sha256=NgYQfYd0bgBjCKcqJdAqIZaf8LM8YxmMQXNfu3aiuKQ,5600
4
+ sysmlv2copilot/client.py,sha256=9haAmRTDyJ0sPeQuSfZAD820ak4q_FiXLZ__c5DEwc8,6327
5
+ sysmlv2copilot/exceptions.py,sha256=CciML13L2vi0ykFLBVKvnykCayo_lJESqSPW9yvDHo0,893
6
+ sysmlv2copilot/types.py,sha256=4UC0ln0kpjwgxKpFpxw5o1A5zW6PYheOXQB7ESb5iII,3097
7
+ sysmlv2copilot-0.2.0.dist-info/licenses/LICENSE,sha256=9If2iTgJ6D92J8kGny9XF5J_30GCiu_OCmu7rh0pQRQ,1070
8
+ sysmlv2copilot-0.2.0.dist-info/METADATA,sha256=GlA-RSzUKNv5FcLY8AnLG-4_yYx-56Hp1N-HgpZBVzM,13263
9
+ sysmlv2copilot-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ sysmlv2copilot-0.2.0.dist-info/entry_points.txt,sha256=kvZa4fSgRoJjdaBg3xyyenQhULsfLQsqAKv9uilcaWw,59
11
+ sysmlv2copilot-0.2.0.dist-info/top_level.txt,sha256=FNO1KVjFkDTO74Lbs9Zyb5yDAmcvleiZ50ap4sPaNGU,15
12
+ sysmlv2copilot-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sysmlv2copilot = sysmlv2copilot.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chance LaVoie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sysmlv2copilot