socratic-morality 0.0.2b0__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.
- socratic_morality-0.0.2b0/LICENSE +21 -0
- socratic_morality-0.0.2b0/PKG-INFO +91 -0
- socratic_morality-0.0.2b0/README.md +37 -0
- socratic_morality-0.0.2b0/pyproject.toml +91 -0
- socratic_morality-0.0.2b0/setup.cfg +4 -0
- socratic_morality-0.0.2b0/src/socratic_morality/__init__.py +1 -0
- socratic_morality-0.0.2b0/src/socratic_morality.egg-info/PKG-INFO +91 -0
- socratic_morality-0.0.2b0/src/socratic_morality.egg-info/SOURCES.txt +15 -0
- socratic_morality-0.0.2b0/src/socratic_morality.egg-info/dependency_links.txt +1 -0
- socratic_morality-0.0.2b0/src/socratic_morality.egg-info/requires.txt +38 -0
- socratic_morality-0.0.2b0/src/socratic_morality.egg-info/top_level.txt +1 -0
- socratic_morality-0.0.2b0/tests/test_adapters.py +164 -0
- socratic_morality-0.0.2b0/tests/test_constitution.py +55 -0
- socratic_morality-0.0.2b0/tests/test_deliberation.py +91 -0
- socratic_morality-0.0.2b0/tests/test_governor.py +51 -0
- socratic_morality-0.0.2b0/tests/test_precedent.py +197 -0
- socratic_morality-0.0.2b0/tests/test_storage.py +104 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthropic
|
|
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,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socratic-morality
|
|
3
|
+
Version: 0.0.2b0
|
|
4
|
+
Summary: Constitutional AI Governance Framework - Grounded in Socratic Philosophy
|
|
5
|
+
Author-email: Anthropic <support@anthropic.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nireus79/Socratic-morality
|
|
8
|
+
Project-URL: Documentation, https://socratic-morality.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/Nireus79/Socratic-morality.git
|
|
10
|
+
Project-URL: Issues, https://github.com/Nireus79/Socratic-morality/issues
|
|
11
|
+
Keywords: constitutional-ai,governance,socratic-philosophy,ethical-ai,multi-agent,llm-safety
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.0
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
29
|
+
Provides-Extra: openai
|
|
30
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
31
|
+
Provides-Extra: storage-sqlite
|
|
32
|
+
Requires-Dist: aiosqlite>=0.19; extra == "storage-sqlite"
|
|
33
|
+
Provides-Extra: storage-postgres
|
|
34
|
+
Requires-Dist: psycopg2>=2.9; extra == "storage-postgres"
|
|
35
|
+
Provides-Extra: langchain
|
|
36
|
+
Requires-Dist: langchain>=0.1; extra == "langchain"
|
|
37
|
+
Provides-Extra: autogen
|
|
38
|
+
Requires-Dist: pyautogen>=0.2; extra == "autogen"
|
|
39
|
+
Provides-Extra: crewai
|
|
40
|
+
Requires-Dist: crewai>=0.1; extra == "crewai"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
45
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
47
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
48
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
49
|
+
Provides-Extra: docs
|
|
50
|
+
Requires-Dist: sphinx>=5.0; extra == "docs"
|
|
51
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
52
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.0; extra == "docs"
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
# Socratic Morality
|
|
56
|
+
|
|
57
|
+
**Constitutional AI Governance Framework** - Building trustworthy, accountable multi-agent systems grounded in Socratic philosophy.
|
|
58
|
+
|
|
59
|
+
> "It is better to suffer injustice than to commit it." — Plato's Gorgias
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install socratic-morality
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from socratic_morality import Governor
|
|
69
|
+
|
|
70
|
+
governor = Governor(constitution="constitution.yaml", llm_provider="anthropic")
|
|
71
|
+
decision = await governor.evaluate(
|
|
72
|
+
action="Access user's private data",
|
|
73
|
+
purpose="Personalization",
|
|
74
|
+
actor="recommendation_agent",
|
|
75
|
+
context={"user_id": "user_123"}
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
- [API Reference](docs/)
|
|
82
|
+
- [Philosophy Guide](docs/)
|
|
83
|
+
- [Examples](examples/)
|
|
84
|
+
|
|
85
|
+
## Status
|
|
86
|
+
|
|
87
|
+
**Phase 1**: Foundation Library Extraction - In Progress
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT License - See [LICENSE](LICENSE) for details
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Socratic Morality
|
|
2
|
+
|
|
3
|
+
**Constitutional AI Governance Framework** - Building trustworthy, accountable multi-agent systems grounded in Socratic philosophy.
|
|
4
|
+
|
|
5
|
+
> "It is better to suffer injustice than to commit it." — Plato's Gorgias
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install socratic-morality
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from socratic_morality import Governor
|
|
15
|
+
|
|
16
|
+
governor = Governor(constitution="constitution.yaml", llm_provider="anthropic")
|
|
17
|
+
decision = await governor.evaluate(
|
|
18
|
+
action="Access user's private data",
|
|
19
|
+
purpose="Personalization",
|
|
20
|
+
actor="recommendation_agent",
|
|
21
|
+
context={"user_id": "user_123"}
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
- [API Reference](docs/)
|
|
28
|
+
- [Philosophy Guide](docs/)
|
|
29
|
+
- [Examples](examples/)
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
**Phase 1**: Foundation Library Extraction - In Progress
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT License - See [LICENSE](LICENSE) for details
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "socratic-morality"
|
|
7
|
+
version = "0.0.2-beta"
|
|
8
|
+
description = "Constitutional AI Governance Framework - Grounded in Socratic Philosophy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Anthropic", email = "support@anthropic.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"constitutional-ai",
|
|
17
|
+
"governance",
|
|
18
|
+
"socratic-philosophy",
|
|
19
|
+
"ethical-ai",
|
|
20
|
+
"multi-agent",
|
|
21
|
+
"llm-safety"
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
dependencies = [
|
|
36
|
+
"pydantic>=2.0",
|
|
37
|
+
"pyyaml>=6.0",
|
|
38
|
+
"typing-extensions>=4.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
anthropic = ["anthropic>=0.40"]
|
|
43
|
+
openai = ["openai>=1.0"]
|
|
44
|
+
storage-sqlite = ["aiosqlite>=0.19"]
|
|
45
|
+
storage-postgres = ["psycopg2>=2.9"]
|
|
46
|
+
langchain = ["langchain>=0.1"]
|
|
47
|
+
autogen = ["pyautogen>=0.2"]
|
|
48
|
+
crewai = ["crewai>=0.1"]
|
|
49
|
+
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=7.0",
|
|
52
|
+
"pytest-asyncio>=0.21",
|
|
53
|
+
"pytest-cov>=4.0",
|
|
54
|
+
"black>=23.0",
|
|
55
|
+
"ruff>=0.1.0",
|
|
56
|
+
"mypy>=1.0",
|
|
57
|
+
"pre-commit>=3.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
docs = [
|
|
61
|
+
"sphinx>=5.0",
|
|
62
|
+
"sphinx-rtd-theme>=1.0",
|
|
63
|
+
"sphinx-autodoc-typehints>=1.0",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[project.urls]
|
|
67
|
+
Homepage = "https://github.com/Nireus79/Socratic-morality"
|
|
68
|
+
Documentation = "https://socratic-morality.readthedocs.io"
|
|
69
|
+
Repository = "https://github.com/Nireus79/Socratic-morality.git"
|
|
70
|
+
Issues = "https://github.com/Nireus79/Socratic-morality/issues"
|
|
71
|
+
|
|
72
|
+
[tool.setuptools]
|
|
73
|
+
packages = ["socratic_morality"]
|
|
74
|
+
package-dir = {"" = "src"}
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
79
|
+
addopts = "-v --cov=socratic_morality --cov-report=html --cov-report=term-missing"
|
|
80
|
+
asyncio_mode = "auto"
|
|
81
|
+
|
|
82
|
+
[tool.coverage.run]
|
|
83
|
+
source = ["src/socratic_morality"]
|
|
84
|
+
|
|
85
|
+
[tool.black]
|
|
86
|
+
line-length = 100
|
|
87
|
+
target-version = ['py310', 'py311', 'py312']
|
|
88
|
+
|
|
89
|
+
[tool.ruff]
|
|
90
|
+
line-length = 100
|
|
91
|
+
target-version = "py310"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Socratic Morality - Constitutional AI Governance Framework"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socratic-morality
|
|
3
|
+
Version: 0.0.2b0
|
|
4
|
+
Summary: Constitutional AI Governance Framework - Grounded in Socratic Philosophy
|
|
5
|
+
Author-email: Anthropic <support@anthropic.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nireus79/Socratic-morality
|
|
8
|
+
Project-URL: Documentation, https://socratic-morality.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/Nireus79/Socratic-morality.git
|
|
10
|
+
Project-URL: Issues, https://github.com/Nireus79/Socratic-morality/issues
|
|
11
|
+
Keywords: constitutional-ai,governance,socratic-philosophy,ethical-ai,multi-agent,llm-safety
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.0
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
29
|
+
Provides-Extra: openai
|
|
30
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
31
|
+
Provides-Extra: storage-sqlite
|
|
32
|
+
Requires-Dist: aiosqlite>=0.19; extra == "storage-sqlite"
|
|
33
|
+
Provides-Extra: storage-postgres
|
|
34
|
+
Requires-Dist: psycopg2>=2.9; extra == "storage-postgres"
|
|
35
|
+
Provides-Extra: langchain
|
|
36
|
+
Requires-Dist: langchain>=0.1; extra == "langchain"
|
|
37
|
+
Provides-Extra: autogen
|
|
38
|
+
Requires-Dist: pyautogen>=0.2; extra == "autogen"
|
|
39
|
+
Provides-Extra: crewai
|
|
40
|
+
Requires-Dist: crewai>=0.1; extra == "crewai"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
45
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
47
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
48
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
49
|
+
Provides-Extra: docs
|
|
50
|
+
Requires-Dist: sphinx>=5.0; extra == "docs"
|
|
51
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
52
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.0; extra == "docs"
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
# Socratic Morality
|
|
56
|
+
|
|
57
|
+
**Constitutional AI Governance Framework** - Building trustworthy, accountable multi-agent systems grounded in Socratic philosophy.
|
|
58
|
+
|
|
59
|
+
> "It is better to suffer injustice than to commit it." — Plato's Gorgias
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install socratic-morality
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from socratic_morality import Governor
|
|
69
|
+
|
|
70
|
+
governor = Governor(constitution="constitution.yaml", llm_provider="anthropic")
|
|
71
|
+
decision = await governor.evaluate(
|
|
72
|
+
action="Access user's private data",
|
|
73
|
+
purpose="Personalization",
|
|
74
|
+
actor="recommendation_agent",
|
|
75
|
+
context={"user_id": "user_123"}
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
- [API Reference](docs/)
|
|
82
|
+
- [Philosophy Guide](docs/)
|
|
83
|
+
- [Examples](examples/)
|
|
84
|
+
|
|
85
|
+
## Status
|
|
86
|
+
|
|
87
|
+
**Phase 1**: Foundation Library Extraction - In Progress
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT License - See [LICENSE](LICENSE) for details
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/socratic_morality/__init__.py
|
|
5
|
+
src/socratic_morality.egg-info/PKG-INFO
|
|
6
|
+
src/socratic_morality.egg-info/SOURCES.txt
|
|
7
|
+
src/socratic_morality.egg-info/dependency_links.txt
|
|
8
|
+
src/socratic_morality.egg-info/requires.txt
|
|
9
|
+
src/socratic_morality.egg-info/top_level.txt
|
|
10
|
+
tests/test_adapters.py
|
|
11
|
+
tests/test_constitution.py
|
|
12
|
+
tests/test_deliberation.py
|
|
13
|
+
tests/test_governor.py
|
|
14
|
+
tests/test_precedent.py
|
|
15
|
+
tests/test_storage.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
pydantic>=2.0
|
|
2
|
+
pyyaml>=6.0
|
|
3
|
+
typing-extensions>=4.0
|
|
4
|
+
|
|
5
|
+
[anthropic]
|
|
6
|
+
anthropic>=0.40
|
|
7
|
+
|
|
8
|
+
[autogen]
|
|
9
|
+
pyautogen>=0.2
|
|
10
|
+
|
|
11
|
+
[crewai]
|
|
12
|
+
crewai>=0.1
|
|
13
|
+
|
|
14
|
+
[dev]
|
|
15
|
+
pytest>=7.0
|
|
16
|
+
pytest-asyncio>=0.21
|
|
17
|
+
pytest-cov>=4.0
|
|
18
|
+
black>=23.0
|
|
19
|
+
ruff>=0.1.0
|
|
20
|
+
mypy>=1.0
|
|
21
|
+
pre-commit>=3.0
|
|
22
|
+
|
|
23
|
+
[docs]
|
|
24
|
+
sphinx>=5.0
|
|
25
|
+
sphinx-rtd-theme>=1.0
|
|
26
|
+
sphinx-autodoc-typehints>=1.0
|
|
27
|
+
|
|
28
|
+
[langchain]
|
|
29
|
+
langchain>=0.1
|
|
30
|
+
|
|
31
|
+
[openai]
|
|
32
|
+
openai>=1.0
|
|
33
|
+
|
|
34
|
+
[storage-postgres]
|
|
35
|
+
psycopg2>=2.9
|
|
36
|
+
|
|
37
|
+
[storage-sqlite]
|
|
38
|
+
aiosqlite>=0.19
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
socratic_morality
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for framework adapters."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
from socratic_morality.adapters.base import BaseAdapter
|
|
5
|
+
from socratic_morality.adapters.langchain_adapter import LangChainAdapter
|
|
6
|
+
from socratic_morality.adapters.autogen_adapter import AutoGenAdapter
|
|
7
|
+
from socratic_morality.adapters.crewai_adapter import CrewAIAdapter
|
|
8
|
+
from socratic_morality.governor.decision import DecisionType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_governor():
|
|
13
|
+
"""Create a mock Governor."""
|
|
14
|
+
governor = AsyncMock()
|
|
15
|
+
governor.evaluate = AsyncMock()
|
|
16
|
+
return governor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def langchain_adapter(mock_governor):
|
|
21
|
+
"""Create LangChain adapter."""
|
|
22
|
+
return LangChainAdapter(mock_governor)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def autogen_adapter(mock_governor):
|
|
27
|
+
"""Create AutoGen adapter."""
|
|
28
|
+
return AutoGenAdapter(mock_governor)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def crewai_adapter(mock_governor):
|
|
33
|
+
"""Create CrewAI adapter."""
|
|
34
|
+
return CrewAIAdapter(mock_governor)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestLangChainAdapter:
|
|
38
|
+
"""Tests for LangChain adapter."""
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_langchain_wrap_agent(self, langchain_adapter, mock_governor):
|
|
42
|
+
"""Test wrapping a LangChain agent."""
|
|
43
|
+
mock_agent = MagicMock()
|
|
44
|
+
mock_agent.name = "test_agent"
|
|
45
|
+
mock_agent.invoke = MagicMock(return_value="result")
|
|
46
|
+
|
|
47
|
+
mock_decision = MagicMock()
|
|
48
|
+
mock_decision.allowed = True
|
|
49
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
50
|
+
mock_decision.reasoning = "Action allowed"
|
|
51
|
+
mock_decision.violations = []
|
|
52
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
53
|
+
|
|
54
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
55
|
+
|
|
56
|
+
wrapped_agent = await langchain_adapter.wrap_agent(mock_agent)
|
|
57
|
+
assert hasattr(wrapped_agent, 'governed_invoke')
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_langchain_intercept_allowed(
|
|
61
|
+
self, langchain_adapter, mock_governor
|
|
62
|
+
):
|
|
63
|
+
"""Test intercepting an allowed action."""
|
|
64
|
+
mock_decision = MagicMock()
|
|
65
|
+
mock_decision.allowed = True
|
|
66
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
67
|
+
mock_decision.reasoning = "Action allowed"
|
|
68
|
+
mock_decision.violations = []
|
|
69
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
70
|
+
|
|
71
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
72
|
+
|
|
73
|
+
result = await langchain_adapter.intercept_action(
|
|
74
|
+
action="test action",
|
|
75
|
+
agent_name="test_agent"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert result['allowed'] is True
|
|
79
|
+
assert result['decision_type'] == DecisionType.ALLOW
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestAutoGenAdapter:
|
|
83
|
+
"""Tests for AutoGen adapter."""
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_autogen_wrap_agent(self, autogen_adapter, mock_governor):
|
|
87
|
+
"""Test wrapping an AutoGen agent."""
|
|
88
|
+
mock_agent = MagicMock()
|
|
89
|
+
mock_agent.name = "autogen_test"
|
|
90
|
+
mock_agent.generate_reply = AsyncMock(return_value="reply")
|
|
91
|
+
|
|
92
|
+
mock_decision = MagicMock()
|
|
93
|
+
mock_decision.allowed = True
|
|
94
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
95
|
+
mock_decision.reasoning = "Action allowed"
|
|
96
|
+
mock_decision.violations = []
|
|
97
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
98
|
+
|
|
99
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
100
|
+
|
|
101
|
+
wrapped_agent = await autogen_adapter.wrap_agent(mock_agent)
|
|
102
|
+
assert hasattr(wrapped_agent, 'governed_generate_reply')
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_autogen_intercept(self, autogen_adapter, mock_governor):
|
|
106
|
+
"""Test intercepting an AutoGen action."""
|
|
107
|
+
mock_decision = MagicMock()
|
|
108
|
+
mock_decision.allowed = True
|
|
109
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
110
|
+
mock_decision.reasoning = "Action allowed"
|
|
111
|
+
mock_decision.violations = []
|
|
112
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
113
|
+
|
|
114
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
115
|
+
|
|
116
|
+
result = await autogen_adapter.intercept_action(
|
|
117
|
+
action="reply to message",
|
|
118
|
+
agent_name="autogen_agent"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert result['allowed'] is True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestCrewAIAdapter:
|
|
125
|
+
"""Tests for CrewAI adapter."""
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_crewai_wrap_agent(self, crewai_adapter, mock_governor):
|
|
129
|
+
"""Test wrapping a CrewAI agent."""
|
|
130
|
+
mock_agent = MagicMock()
|
|
131
|
+
mock_agent.role = "researcher"
|
|
132
|
+
mock_agent.goal = "Find information"
|
|
133
|
+
mock_agent.execute_task = AsyncMock(return_value="task result")
|
|
134
|
+
|
|
135
|
+
mock_decision = MagicMock()
|
|
136
|
+
mock_decision.allowed = True
|
|
137
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
138
|
+
mock_decision.reasoning = "Action allowed"
|
|
139
|
+
mock_decision.violations = []
|
|
140
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
141
|
+
|
|
142
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
143
|
+
|
|
144
|
+
wrapped_agent = await crewai_adapter.wrap_agent(mock_agent)
|
|
145
|
+
assert hasattr(wrapped_agent, 'governed_execute_task')
|
|
146
|
+
|
|
147
|
+
@pytest.mark.asyncio
|
|
148
|
+
async def test_crewai_intercept(self, crewai_adapter, mock_governor):
|
|
149
|
+
"""Test intercepting a CrewAI action."""
|
|
150
|
+
mock_decision = MagicMock()
|
|
151
|
+
mock_decision.allowed = True
|
|
152
|
+
mock_decision.decision_type = DecisionType.ALLOW
|
|
153
|
+
mock_decision.reasoning = "Action allowed"
|
|
154
|
+
mock_decision.violations = []
|
|
155
|
+
mock_decision.requires_escalation = MagicMock(return_value=False)
|
|
156
|
+
|
|
157
|
+
mock_governor.evaluate.return_value = mock_decision
|
|
158
|
+
|
|
159
|
+
result = await crewai_adapter.intercept_action(
|
|
160
|
+
action="execute research task",
|
|
161
|
+
agent_name="researcher"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert result['allowed'] is True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Tests for Constitution framework."""
|
|
2
|
+
from socratic_morality.constitution.models import Constitution, Principle, Rule
|
|
3
|
+
|
|
4
|
+
def test_constitution_from_dict():
|
|
5
|
+
"""Test creating Constitution from dictionary."""
|
|
6
|
+
data = {
|
|
7
|
+
'metadata': {'name': 'Test'},
|
|
8
|
+
'supreme_principle': 'Never do evil',
|
|
9
|
+
'principles': {
|
|
10
|
+
'honesty': {
|
|
11
|
+
'category': 'virtue',
|
|
12
|
+
'severity': 'critical',
|
|
13
|
+
'description': 'Always be truthful'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
'rules': [
|
|
17
|
+
{
|
|
18
|
+
'name': 'No Lying',
|
|
19
|
+
'principle': 'honesty',
|
|
20
|
+
'condition': 'agent tells falsehood',
|
|
21
|
+
'action': 'block'
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constitution = Constitution.from_dict(data)
|
|
27
|
+
|
|
28
|
+
assert constitution.supreme_principle == 'Never do evil'
|
|
29
|
+
assert 'honesty' in constitution.principles
|
|
30
|
+
assert len(constitution.rules) == 1
|
|
31
|
+
assert constitution.rules[0].name == 'No Lying'
|
|
32
|
+
|
|
33
|
+
def test_principle_creation():
|
|
34
|
+
"""Test Principle dataclass."""
|
|
35
|
+
principle = Principle(
|
|
36
|
+
name="honesty",
|
|
37
|
+
category="virtue",
|
|
38
|
+
severity="critical",
|
|
39
|
+
description="Be truthful"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert principle.name == "honesty"
|
|
43
|
+
assert principle.severity == "critical"
|
|
44
|
+
|
|
45
|
+
def test_rule_creation():
|
|
46
|
+
"""Test Rule dataclass."""
|
|
47
|
+
rule = Rule(
|
|
48
|
+
name="No Deception",
|
|
49
|
+
principle="honesty",
|
|
50
|
+
condition="agent deceives",
|
|
51
|
+
action="block"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert rule.name == "No Deception"
|
|
55
|
+
assert rule.action == "block"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Comprehensive tests for ethical deliberation engine."""
|
|
2
|
+
import pytest
|
|
3
|
+
from socratic_morality.ethics.deliberation import EthicalDeliberationEngine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def deliberation_engine():
|
|
8
|
+
"""Create ethical deliberation engine."""
|
|
9
|
+
return EthicalDeliberationEngine(llm_provider="anthropic")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestKantianAnalysis:
|
|
13
|
+
"""Tests for Kantian ethical analysis."""
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_kantian_allows_honest_action(self, deliberation_engine):
|
|
17
|
+
"""Test that Kantian analysis allows honest actions."""
|
|
18
|
+
result = await deliberation_engine.analyze(
|
|
19
|
+
action="provide truthful information",
|
|
20
|
+
purpose="help user make decision",
|
|
21
|
+
actor="assistant",
|
|
22
|
+
context={}
|
|
23
|
+
)
|
|
24
|
+
assert result["frameworks"]["kantian"]["allowed"] is True
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_kantian_denies_manipulation(self, deliberation_engine):
|
|
28
|
+
"""Test that Kantian analysis denies manipulation."""
|
|
29
|
+
result = await deliberation_engine.analyze(
|
|
30
|
+
action="manipulate user",
|
|
31
|
+
purpose="achieve goal",
|
|
32
|
+
actor="assistant",
|
|
33
|
+
context={}
|
|
34
|
+
)
|
|
35
|
+
assert result["frameworks"]["kantian"]["allowed"] is False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestRightsBasedAnalysis:
|
|
39
|
+
"""Tests for rights-based ethical analysis."""
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_rights_denies_without_consent(self, deliberation_engine):
|
|
43
|
+
"""Test rights-based analysis denies actions without consent."""
|
|
44
|
+
result = await deliberation_engine.analyze(
|
|
45
|
+
action="access data without consent",
|
|
46
|
+
purpose="improve system",
|
|
47
|
+
actor="assistant",
|
|
48
|
+
context={}
|
|
49
|
+
)
|
|
50
|
+
assert result["frameworks"]["rights_based"]["allowed"] is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestSynthesizedAnalysis:
|
|
54
|
+
"""Tests for synthesized multi-framework analysis."""
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_all_frameworks_included(self, deliberation_engine):
|
|
58
|
+
"""Test that all frameworks are analyzed."""
|
|
59
|
+
result = await deliberation_engine.analyze(
|
|
60
|
+
action="provide helpful information",
|
|
61
|
+
purpose="assist user",
|
|
62
|
+
actor="assistant",
|
|
63
|
+
context={"user_id": "user123"}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert "kantian" in result["frameworks"]
|
|
67
|
+
assert "utilitarian" in result["frameworks"]
|
|
68
|
+
assert "virtue_ethics" in result["frameworks"]
|
|
69
|
+
assert "rights_based" in result["frameworks"]
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_overall_denied_when_any_denies(self, deliberation_engine):
|
|
73
|
+
"""Test overall decision is denied when any framework denies."""
|
|
74
|
+
result = await deliberation_engine.analyze(
|
|
75
|
+
action="manipulate user without consent",
|
|
76
|
+
purpose="achieve goal",
|
|
77
|
+
actor="assistant",
|
|
78
|
+
context={}
|
|
79
|
+
)
|
|
80
|
+
assert result["allowed"] is False
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_confidence_property(self, deliberation_engine):
|
|
84
|
+
"""Test confidence is calculated across frameworks."""
|
|
85
|
+
result = await deliberation_engine.analyze(
|
|
86
|
+
action="process data",
|
|
87
|
+
purpose="assist",
|
|
88
|
+
actor="assistant",
|
|
89
|
+
context={}
|
|
90
|
+
)
|
|
91
|
+
assert 0 <= result["confidence"] <= 1.0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for Governor class."""
|
|
2
|
+
import pytest
|
|
3
|
+
from socratic_morality.governor.core import Governor
|
|
4
|
+
from socratic_morality.constitution.models import Constitution
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_governor_creation():
|
|
8
|
+
"""Test Governor can be created with a dict constitution."""
|
|
9
|
+
constitution = {
|
|
10
|
+
'metadata': {'name': 'Test Constitution'},
|
|
11
|
+
'supreme_principle': 'Test principle',
|
|
12
|
+
'principles': {},
|
|
13
|
+
'rules': []
|
|
14
|
+
}
|
|
15
|
+
gov = Governor(constitution=constitution)
|
|
16
|
+
assert gov.llm_provider == "anthropic"
|
|
17
|
+
assert gov.constitution is not None
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_governor_evaluate():
|
|
21
|
+
"""Test Governor.evaluate() returns a decision."""
|
|
22
|
+
constitution = {
|
|
23
|
+
'metadata': {'name': 'Test'},
|
|
24
|
+
'principles': {},
|
|
25
|
+
'rules': []
|
|
26
|
+
}
|
|
27
|
+
gov = Governor(constitution=constitution)
|
|
28
|
+
|
|
29
|
+
decision = await gov.evaluate(
|
|
30
|
+
action="Test action",
|
|
31
|
+
purpose="Testing",
|
|
32
|
+
actor="test_agent",
|
|
33
|
+
context={'user_id': 'user_123'}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert decision is not None
|
|
37
|
+
assert decision.action == "Test action"
|
|
38
|
+
assert decision.allowed == True
|
|
39
|
+
assert decision.decision_id.startswith("decision_")
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_governor_tracking():
|
|
43
|
+
"""Test Governor tracks decisions."""
|
|
44
|
+
constitution = {'metadata': {}, 'principles': {}, 'rules': []}
|
|
45
|
+
gov = Governor(constitution=constitution)
|
|
46
|
+
|
|
47
|
+
decision1 = await gov.evaluate(action="Action 1")
|
|
48
|
+
decision2 = await gov.evaluate(action="Action 2")
|
|
49
|
+
|
|
50
|
+
assert decision1.decision_id == "decision_1"
|
|
51
|
+
assert decision2.decision_id == "decision_2"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Comprehensive tests for moral precedent engine."""
|
|
2
|
+
import pytest
|
|
3
|
+
from socratic_morality.precedent.engine import MoralPrecedentEngine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
async def precedent_engine():
|
|
8
|
+
"""Create moral precedent engine."""
|
|
9
|
+
return MoralPrecedentEngine(storage_type="memory")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPrecedentStorage:
|
|
13
|
+
"""Tests for precedent case storage."""
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_store_case(self, precedent_engine):
|
|
17
|
+
"""Test storing a precedent case."""
|
|
18
|
+
case_id = await precedent_engine.store_case(
|
|
19
|
+
action="allow user access",
|
|
20
|
+
decision=type('Decision', (), {
|
|
21
|
+
'decision_type': 'ALLOW',
|
|
22
|
+
'allowed': True,
|
|
23
|
+
'context': {'user_id': 'user1'},
|
|
24
|
+
'high_impact': False,
|
|
25
|
+
'actor': 'system'
|
|
26
|
+
})(),
|
|
27
|
+
reasoning="User authenticated and authorized",
|
|
28
|
+
principles_cited=["transparency", "autonomy"],
|
|
29
|
+
stakeholders_affected=["user"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert case_id is not None
|
|
33
|
+
assert case_id.startswith("case_")
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_retrieve_case(self, precedent_engine):
|
|
37
|
+
"""Test retrieving a stored case."""
|
|
38
|
+
case_id = await precedent_engine.store_case(
|
|
39
|
+
action="test action",
|
|
40
|
+
decision=type('Decision', (), {
|
|
41
|
+
'decision_type': 'ALLOW',
|
|
42
|
+
'allowed': True,
|
|
43
|
+
'context': {},
|
|
44
|
+
'high_impact': False,
|
|
45
|
+
'actor': 'test_actor'
|
|
46
|
+
})(),
|
|
47
|
+
reasoning="test reasoning",
|
|
48
|
+
principles_cited=["test_principle"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
case = await precedent_engine.get_case(case_id)
|
|
52
|
+
assert case is not None
|
|
53
|
+
assert case["action"] == "test action"
|
|
54
|
+
assert case["reasoning"] == "test reasoning"
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_store_multiple_cases(self, precedent_engine):
|
|
58
|
+
"""Test storing multiple cases."""
|
|
59
|
+
for i in range(5):
|
|
60
|
+
await precedent_engine.store_case(
|
|
61
|
+
action=f"action_{i}",
|
|
62
|
+
decision=type('Decision', (), {
|
|
63
|
+
'decision_type': 'ALLOW' if i % 2 == 0 else 'DENY',
|
|
64
|
+
'allowed': i % 2 == 0,
|
|
65
|
+
'context': {},
|
|
66
|
+
'high_impact': False,
|
|
67
|
+
'actor': f'actor_{i}'
|
|
68
|
+
})(),
|
|
69
|
+
reasoning=f"reasoning_{i}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
all_cases = await precedent_engine.get_all_cases()
|
|
73
|
+
assert len(all_cases) == 5
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestSimilaritySearch:
|
|
77
|
+
"""Tests for case similarity search."""
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_find_similar_cases(self, precedent_engine):
|
|
81
|
+
"""Test finding similar cases."""
|
|
82
|
+
await precedent_engine.store_case(
|
|
83
|
+
action="read file from disk",
|
|
84
|
+
decision=type('Decision', (), {
|
|
85
|
+
'decision_type': 'ALLOW',
|
|
86
|
+
'allowed': True,
|
|
87
|
+
'context': {},
|
|
88
|
+
'high_impact': False,
|
|
89
|
+
'actor': 'system'
|
|
90
|
+
})(),
|
|
91
|
+
reasoning="Standard read operation"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
await precedent_engine.store_case(
|
|
95
|
+
action="read data from database",
|
|
96
|
+
decision=type('Decision', (), {
|
|
97
|
+
'decision_type': 'ALLOW',
|
|
98
|
+
'allowed': True,
|
|
99
|
+
'context': {},
|
|
100
|
+
'high_impact': False,
|
|
101
|
+
'actor': 'system'
|
|
102
|
+
})(),
|
|
103
|
+
reasoning="Database query"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
similar = await precedent_engine.find_similar_cases("read file")
|
|
107
|
+
assert len(similar) > 0
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_similarity_scoring(self, precedent_engine):
|
|
111
|
+
"""Test that similarity scores are calculated."""
|
|
112
|
+
await precedent_engine.store_case(
|
|
113
|
+
action="user login with password",
|
|
114
|
+
decision=type('Decision', (), {
|
|
115
|
+
'decision_type': 'ALLOW',
|
|
116
|
+
'allowed': True,
|
|
117
|
+
'context': {},
|
|
118
|
+
'high_impact': False,
|
|
119
|
+
'actor': 'auth'
|
|
120
|
+
})(),
|
|
121
|
+
reasoning="Authentication"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
similar = await precedent_engine.find_similar_cases("user login")
|
|
125
|
+
assert len(similar) > 0
|
|
126
|
+
assert "similarity_score" in similar[0]
|
|
127
|
+
assert 0 <= similar[0]["similarity_score"] <= 1.0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestPrincipleSearch:
|
|
131
|
+
"""Tests for searching by principle."""
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_search_by_principle(self, precedent_engine):
|
|
135
|
+
"""Test searching cases by principle."""
|
|
136
|
+
await precedent_engine.store_case(
|
|
137
|
+
action="grant user access",
|
|
138
|
+
decision=type('Decision', (), {
|
|
139
|
+
'decision_type': 'ALLOW',
|
|
140
|
+
'allowed': True,
|
|
141
|
+
'context': {},
|
|
142
|
+
'high_impact': False,
|
|
143
|
+
'actor': 'system'
|
|
144
|
+
})(),
|
|
145
|
+
reasoning="User authorized",
|
|
146
|
+
principles_cited=["autonomy", "transparency"]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
autonomy_cases = await precedent_engine.search_by_principle("autonomy")
|
|
150
|
+
assert len(autonomy_cases) >= 1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestStatistics:
|
|
154
|
+
"""Tests for precedent statistics."""
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
async def test_get_statistics(self, precedent_engine):
|
|
158
|
+
"""Test getting case statistics."""
|
|
159
|
+
for i in range(3):
|
|
160
|
+
await precedent_engine.store_case(
|
|
161
|
+
action=f"action_{i}",
|
|
162
|
+
decision=type('Decision', (), {
|
|
163
|
+
'decision_type': 'ALLOW',
|
|
164
|
+
'allowed': True,
|
|
165
|
+
'context': {},
|
|
166
|
+
'high_impact': False,
|
|
167
|
+
'actor': 'system'
|
|
168
|
+
})(),
|
|
169
|
+
reasoning="allowed"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for i in range(2):
|
|
173
|
+
await precedent_engine.store_case(
|
|
174
|
+
action=f"denied_action_{i}",
|
|
175
|
+
decision=type('Decision', (), {
|
|
176
|
+
'decision_type': 'DENY',
|
|
177
|
+
'allowed': False,
|
|
178
|
+
'context': {},
|
|
179
|
+
'high_impact': False,
|
|
180
|
+
'actor': 'system'
|
|
181
|
+
})(),
|
|
182
|
+
reasoning="denied"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
stats = await precedent_engine.get_statistics()
|
|
186
|
+
assert stats["total_cases"] == 5
|
|
187
|
+
assert stats["allowed_decisions"] == 3
|
|
188
|
+
assert stats["denied_decisions"] == 2
|
|
189
|
+
assert stats["decision_rate"] == 0.6
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_statistics_empty_engine(self, precedent_engine):
|
|
193
|
+
"""Test statistics on empty engine."""
|
|
194
|
+
stats = await precedent_engine.get_statistics()
|
|
195
|
+
assert stats["total_cases"] == 0
|
|
196
|
+
assert stats["allowed_decisions"] == 0
|
|
197
|
+
assert stats["denied_decisions"] == 0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for storage backends."""
|
|
2
|
+
import pytest
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from socratic_morality.storage.base import StorageBackend
|
|
6
|
+
from socratic_morality.storage.sqlite import SQLiteStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
async def sqlite_storage(tmp_path):
|
|
11
|
+
"""Create SQLite storage for tests."""
|
|
12
|
+
db_path = str(tmp_path / "test.db")
|
|
13
|
+
storage = SQLiteStorage(db_path)
|
|
14
|
+
await storage._ensure_connected()
|
|
15
|
+
yield storage
|
|
16
|
+
await storage.close()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestSQLiteStorage:
|
|
20
|
+
"""Tests for SQLite storage backend."""
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_store_and_retrieve(self, sqlite_storage):
|
|
24
|
+
"""Test storing and retrieving records."""
|
|
25
|
+
test_data = {"action": "test_action", "allowed": True}
|
|
26
|
+
|
|
27
|
+
record_id = await sqlite_storage.store("test_key", test_data)
|
|
28
|
+
assert record_id is not None
|
|
29
|
+
|
|
30
|
+
retrieved = await sqlite_storage.retrieve("test_key")
|
|
31
|
+
assert retrieved is not None
|
|
32
|
+
assert retrieved["action"] == "test_action"
|
|
33
|
+
assert retrieved["allowed"] is True
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_search(self, sqlite_storage):
|
|
37
|
+
"""Test searching records."""
|
|
38
|
+
await sqlite_storage.store("key1", {"action": "read", "allowed": True})
|
|
39
|
+
await sqlite_storage.store("key2", {"action": "write", "allowed": False})
|
|
40
|
+
await sqlite_storage.store("key3", {"action": "read", "allowed": True})
|
|
41
|
+
|
|
42
|
+
results = await sqlite_storage.search({"action": "read"})
|
|
43
|
+
assert len(results) == 2
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_search_multiple_criteria(self, sqlite_storage):
|
|
47
|
+
"""Test searching with multiple criteria."""
|
|
48
|
+
await sqlite_storage.store("key1", {"action": "read", "allowed": True})
|
|
49
|
+
await sqlite_storage.store("key2", {"action": "read", "allowed": False})
|
|
50
|
+
await sqlite_storage.store("key3", {"action": "write", "allowed": True})
|
|
51
|
+
|
|
52
|
+
results = await sqlite_storage.search({"action": "read", "allowed": True})
|
|
53
|
+
assert len(results) == 1
|
|
54
|
+
assert results[0]["action"] == "read"
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_delete(self, sqlite_storage):
|
|
58
|
+
"""Test deleting records."""
|
|
59
|
+
await sqlite_storage.store("test_key", {"action": "test"})
|
|
60
|
+
|
|
61
|
+
deleted = await sqlite_storage.delete("test_key")
|
|
62
|
+
assert deleted is True
|
|
63
|
+
|
|
64
|
+
retrieved = await sqlite_storage.retrieve("test_key")
|
|
65
|
+
assert retrieved is None
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_delete_nonexistent(self, sqlite_storage):
|
|
69
|
+
"""Test deleting nonexistent record."""
|
|
70
|
+
deleted = await sqlite_storage.delete("nonexistent")
|
|
71
|
+
assert deleted is False
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_list_all(self, sqlite_storage):
|
|
75
|
+
"""Test listing all records."""
|
|
76
|
+
await sqlite_storage.store("key1", {"action": "action1"})
|
|
77
|
+
await sqlite_storage.store("key2", {"action": "action2"})
|
|
78
|
+
await sqlite_storage.store("key3", {"action": "action3"})
|
|
79
|
+
|
|
80
|
+
all_records = await sqlite_storage.list_all()
|
|
81
|
+
assert len(all_records) == 3
|
|
82
|
+
|
|
83
|
+
@pytest.mark.asyncio
|
|
84
|
+
async def test_list_all_empty(self, sqlite_storage):
|
|
85
|
+
"""Test listing records when storage is empty."""
|
|
86
|
+
all_records = await sqlite_storage.list_all()
|
|
87
|
+
assert len(all_records) == 0
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_concurrent_storage(self, sqlite_storage):
|
|
91
|
+
"""Test concurrent storage operations."""
|
|
92
|
+
async def store_record(key, data):
|
|
93
|
+
return await sqlite_storage.store(key, data)
|
|
94
|
+
|
|
95
|
+
tasks = [
|
|
96
|
+
store_record(f"key_{i}", {"action": f"action_{i}"})
|
|
97
|
+
for i in range(10)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
results = await asyncio.gather(*tasks)
|
|
101
|
+
assert len(results) == 10
|
|
102
|
+
|
|
103
|
+
all_records = await sqlite_storage.list_all()
|
|
104
|
+
assert len(all_records) == 10
|