gaard-llm 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,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: gaard-llm
3
+ Version: 0.1.0
4
+ Summary: LLM provider adapters for GAARD
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: gaard-core==0.1.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pydantic>=2.7.0
10
+ Provides-Extra: openai
11
+ Requires-Dist: openai>=1.0.0; extra == "openai"
12
+ Provides-Extra: anthropic
13
+ Requires-Dist: anthropic>=0.34.0; extra == "anthropic"
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
17
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
18
+
19
+ # GAARD - Governed AI Access to Relational Data
20
+
21
+ GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
22
+
23
+ GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
24
+
25
+ For more informacion see https://github.com/pkroliszewski/gaard
26
+
27
+ # This package
28
+ Package gaard-llm extends gaard functionality by adding support for various ai providers.
@@ -0,0 +1,10 @@
1
+ # GAARD - Governed AI Access to Relational Data
2
+
3
+ GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
4
+
5
+ GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
6
+
7
+ For more informacion see https://github.com/pkroliszewski/gaard
8
+
9
+ # This package
10
+ Package gaard-llm extends gaard functionality by adding support for various ai providers.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gaard-llm"
7
+ version = "0.1.0"
8
+ description = "LLM provider adapters for GAARD"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "gaard-core==0.1.0",
13
+ "httpx>=0.27.0",
14
+ "pydantic>=2.7.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ openai = ["openai>=1.0.0"]
19
+ anthropic = ["anthropic>=0.34.0"]
20
+ dev = [
21
+ "pytest>=8.0.0",
22
+ "ruff>=0.5.0",
23
+ "mypy>=1.10.0",
24
+ ]
25
+
26
+ [tool.ruff]
27
+ line-length = 100
28
+ target-version = "py311"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,74 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from gaard_core.errors import LlmProviderError
6
+ from gaard_llm.providers.models import ChatCompletionRequest, ChatCompletionResponse
7
+
8
+
9
+ class OpenAICompatibleClient:
10
+ def __init__(
11
+ self,
12
+ base_url: str,
13
+ api_key: str,
14
+ timeout_seconds: int = 60,
15
+ ) -> None:
16
+ self.base_url = base_url.rstrip("/")
17
+ self.api_key = api_key
18
+ self.timeout_seconds = timeout_seconds
19
+
20
+ def create_chat_completion(
21
+ self,
22
+ request: ChatCompletionRequest,
23
+ ) -> ChatCompletionResponse:
24
+ url = f"{self.base_url}/chat/completions"
25
+
26
+ payload: dict[str, Any] = {
27
+ "model": request.model,
28
+ "messages": [
29
+ {
30
+ "role": message.role,
31
+ "content": message.content,
32
+ }
33
+ for message in request.messages
34
+ ],
35
+ "temperature": request.temperature,
36
+ }
37
+
38
+ payload.update(request.extra_body)
39
+
40
+ headers = {
41
+ "Authorization": f"Bearer {self.api_key}",
42
+ "Content-Type": "application/json",
43
+ }
44
+
45
+ try:
46
+ response = httpx.post(
47
+ url,
48
+ json=payload,
49
+ headers=headers,
50
+ timeout=self.timeout_seconds,
51
+ )
52
+ response.raise_for_status()
53
+ except httpx.HTTPStatusError as exc:
54
+ detail = exc.response.text.strip()
55
+ detail_suffix = f" {detail[:500]}" if detail else ""
56
+
57
+ raise LlmProviderError(
58
+ f"LLM provider returned HTTP {exc.response.status_code}.{detail_suffix}"
59
+ ) from exc
60
+ except httpx.HTTPError as exc:
61
+ raise LlmProviderError("LLM provider request failed.") from exc
62
+
63
+ data: dict[str, Any] = response.json()
64
+
65
+ try:
66
+ content = data["choices"][0]["message"]["content"]
67
+ except (KeyError, IndexError, TypeError) as exc:
68
+ raise LlmProviderError("Invalid OpenAI-compatible response format.") from exc
69
+
70
+ return ChatCompletionResponse(
71
+ content=content.strip(),
72
+ model=data.get("model"),
73
+ raw=data,
74
+ )
File without changes
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ChatMessage(BaseModel):
7
+ role: str
8
+ content: str
9
+
10
+
11
+ class ChatCompletionRequest(BaseModel):
12
+ model: str
13
+ messages: list[ChatMessage]
14
+ temperature: float = 0.0
15
+ extra_body: dict[str, Any] = Field(default_factory=dict)
16
+
17
+
18
+ class ChatCompletionResponse(BaseModel):
19
+ content: str
20
+ model: str | None = None
21
+ raw: dict[str, Any] = Field(default_factory=dict)
File without changes
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: gaard-llm
3
+ Version: 0.1.0
4
+ Summary: LLM provider adapters for GAARD
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: gaard-core==0.1.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pydantic>=2.7.0
10
+ Provides-Extra: openai
11
+ Requires-Dist: openai>=1.0.0; extra == "openai"
12
+ Provides-Extra: anthropic
13
+ Requires-Dist: anthropic>=0.34.0; extra == "anthropic"
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
17
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
18
+
19
+ # GAARD - Governed AI Access to Relational Data
20
+
21
+ GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
22
+
23
+ GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
24
+
25
+ For more informacion see https://github.com/pkroliszewski/gaard
26
+
27
+ # This package
28
+ Package gaard-llm extends gaard functionality by adding support for various ai providers.
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/gaard_llm/__init__.py
4
+ src/gaard_llm.egg-info/PKG-INFO
5
+ src/gaard_llm.egg-info/SOURCES.txt
6
+ src/gaard_llm.egg-info/dependency_links.txt
7
+ src/gaard_llm.egg-info/requires.txt
8
+ src/gaard_llm.egg-info/top_level.txt
9
+ src/gaard_llm/anthropic/__init__.py
10
+ src/gaard_llm/azure_openai/__init__.py
11
+ src/gaard_llm/ollama/__init__.py
12
+ src/gaard_llm/openai/__init__.py
13
+ src/gaard_llm/openai_compatible/__init__.py
14
+ src/gaard_llm/openai_compatible/client.py
15
+ src/gaard_llm/providers/__init__.py
16
+ src/gaard_llm/providers/models.py
17
+ src/gaard_llm/vertex/__init__.py
18
+ tests/test_openai_compatible_client.py
@@ -0,0 +1,14 @@
1
+ gaard-core==0.1.0
2
+ httpx>=0.27.0
3
+ pydantic>=2.7.0
4
+
5
+ [anthropic]
6
+ anthropic>=0.34.0
7
+
8
+ [dev]
9
+ pytest>=8.0.0
10
+ ruff>=0.5.0
11
+ mypy>=1.10.0
12
+
13
+ [openai]
14
+ openai>=1.0.0
@@ -0,0 +1 @@
1
+ gaard_llm
@@ -0,0 +1,177 @@
1
+ import httpx
2
+ import pytest
3
+
4
+ from gaard_core.errors import LlmProviderError
5
+ from gaard_llm.openai_compatible.client import OpenAICompatibleClient
6
+ from gaard_llm.providers.models import ChatCompletionRequest, ChatMessage
7
+
8
+
9
+ def test_openai_compatible_client_parses_response(monkeypatch) -> None:
10
+ captured_kwargs = {}
11
+
12
+ def fake_post(*args, **kwargs):
13
+ captured_kwargs.update(kwargs)
14
+ request = httpx.Request(
15
+ method="POST",
16
+ url="https://example.com/v1/chat/completions",
17
+ )
18
+
19
+ return httpx.Response(
20
+ status_code=200,
21
+ request=request,
22
+ json={
23
+ "model": "test-model",
24
+ "choices": [
25
+ {
26
+ "message": {
27
+ "content": "SELECT 1 AS value",
28
+ }
29
+ }
30
+ ],
31
+ },
32
+ )
33
+
34
+ monkeypatch.setattr(httpx, "post", fake_post)
35
+
36
+ client = OpenAICompatibleClient(
37
+ base_url="https://example.com/v1",
38
+ api_key="test-key",
39
+ )
40
+
41
+ response = client.create_chat_completion(
42
+ ChatCompletionRequest(
43
+ model="test-model",
44
+ messages=[
45
+ ChatMessage(role="system", content="system"),
46
+ ChatMessage(role="user", content="user"),
47
+ ],
48
+ )
49
+ )
50
+
51
+ assert response.content == "SELECT 1 AS value"
52
+ assert response.model == "test-model"
53
+ assert "chat_template_kwargs" not in captured_kwargs["json"]
54
+
55
+
56
+ def test_openai_compatible_client_merges_extra_body(monkeypatch) -> None:
57
+ captured_kwargs = {}
58
+
59
+ def fake_post(*args, **kwargs):
60
+ captured_kwargs.update(kwargs)
61
+ request = httpx.Request(
62
+ method="POST",
63
+ url="https://example.com/v1/chat/completions",
64
+ )
65
+
66
+ return httpx.Response(
67
+ status_code=200,
68
+ request=request,
69
+ json={
70
+ "model": "test-model",
71
+ "choices": [
72
+ {
73
+ "message": {
74
+ "content": "SELECT 1 AS value",
75
+ }
76
+ }
77
+ ],
78
+ },
79
+ )
80
+
81
+ monkeypatch.setattr(httpx, "post", fake_post)
82
+
83
+ client = OpenAICompatibleClient(
84
+ base_url="https://example.com/v1",
85
+ api_key="test-key",
86
+ )
87
+
88
+ client.create_chat_completion(
89
+ ChatCompletionRequest(
90
+ model="test-model",
91
+ extra_body={
92
+ "chat_template_kwargs": {
93
+ "enable_thinking": False,
94
+ },
95
+ },
96
+ messages=[
97
+ ChatMessage(role="system", content="system"),
98
+ ChatMessage(role="user", content="user"),
99
+ ],
100
+ )
101
+ )
102
+
103
+ assert captured_kwargs["json"]["chat_template_kwargs"] == {"enable_thinking": False}
104
+
105
+
106
+ def test_openai_compatible_client_wraps_invalid_response(monkeypatch) -> None:
107
+ def fake_post(*args, **kwargs):
108
+ request = httpx.Request(
109
+ method="POST",
110
+ url="https://example.com/v1/chat/completions",
111
+ )
112
+
113
+ return httpx.Response(
114
+ status_code=200,
115
+ request=request,
116
+ json={
117
+ "unexpected": "format",
118
+ },
119
+ )
120
+
121
+ monkeypatch.setattr(httpx, "post", fake_post)
122
+
123
+ client = OpenAICompatibleClient(
124
+ base_url="https://example.com/v1",
125
+ api_key="test-key",
126
+ )
127
+
128
+ with pytest.raises(LlmProviderError):
129
+ client.create_chat_completion(
130
+ ChatCompletionRequest(
131
+ model="test-model",
132
+ messages=[
133
+ ChatMessage(role="system", content="system"),
134
+ ChatMessage(role="user", content="user"),
135
+ ],
136
+ )
137
+ )
138
+
139
+
140
+ def test_openai_compatible_client_includes_provider_error_body(monkeypatch) -> None:
141
+ def fake_post(*args, **kwargs):
142
+ request = httpx.Request(
143
+ method="POST",
144
+ url="https://example.com/v1/chat/completions",
145
+ )
146
+
147
+ return httpx.Response(
148
+ status_code=400,
149
+ request=request,
150
+ json={
151
+ "error": {
152
+ "message": "Unrecognized request argument supplied.",
153
+ "type": "invalid_request_error",
154
+ }
155
+ },
156
+ )
157
+
158
+ monkeypatch.setattr(httpx, "post", fake_post)
159
+
160
+ client = OpenAICompatibleClient(
161
+ base_url="https://example.com/v1",
162
+ api_key="test-key",
163
+ )
164
+
165
+ with pytest.raises(LlmProviderError) as exc_info:
166
+ client.create_chat_completion(
167
+ ChatCompletionRequest(
168
+ model="test-model",
169
+ messages=[
170
+ ChatMessage(role="system", content="system"),
171
+ ChatMessage(role="user", content="user"),
172
+ ],
173
+ )
174
+ )
175
+
176
+ assert "LLM provider returned HTTP 400." in str(exc_info.value)
177
+ assert "Unrecognized request argument supplied." in str(exc_info.value)