aec-agent-core 1.0.0.dev0__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.
- aec_agent_core-1.0.0.dev0/LICENSE +21 -0
- aec_agent_core-1.0.0.dev0/PKG-INFO +75 -0
- aec_agent_core-1.0.0.dev0/README.md +37 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core/__init__.py +166 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core.egg-info/PKG-INFO +75 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core.egg-info/SOURCES.txt +10 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core.egg-info/dependency_links.txt +1 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core.egg-info/requires.txt +17 -0
- aec_agent_core-1.0.0.dev0/aec_agent_core.egg-info/top_level.txt +1 -0
- aec_agent_core-1.0.0.dev0/pyproject.toml +77 -0
- aec_agent_core-1.0.0.dev0/setup.cfg +4 -0
- aec_agent_core-1.0.0.dev0/tests/test_core_aliases.py +212 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Sean O'Connell
|
|
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,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aec-agent-core
|
|
3
|
+
Version: 1.0.0.dev0
|
|
4
|
+
Summary: The shared core of LLM agents that review and produce architecture, engineering and construction documents: model connections, vision calls, guards and records, Word and Excel, and a figure helper. No user interface of its own.
|
|
5
|
+
Author-email: Sean O'Connell <soconnell345@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/soconnell345-geotech/GeotechStaffEngineer
|
|
8
|
+
Project-URL: Repository, https://github.com/soconnell345-geotech/GeotechStaffEngineer
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.26
|
|
22
|
+
Requires-Dist: scipy>=1.13
|
|
23
|
+
Requires-Dist: deepagents<0.7.25,>=0.6.8
|
|
24
|
+
Requires-Dist: langchain<1.4.6,>=1.3
|
|
25
|
+
Requires-Dist: langgraph<1.3,>=1.2
|
|
26
|
+
Requires-Dist: typing_extensions>=4.13
|
|
27
|
+
Requires-Dist: openai<3.29.1
|
|
28
|
+
Requires-Dist: langchain-openai>=1.3
|
|
29
|
+
Requires-Dist: matplotlib>=3.8
|
|
30
|
+
Requires-Dist: plotly>=5
|
|
31
|
+
Requires-Dist: python-docx>=1.1
|
|
32
|
+
Requires-Dist: markdown-it-py>=3
|
|
33
|
+
Requires-Dist: openpyxl>=3.1
|
|
34
|
+
Requires-Dist: planlens>=0.14
|
|
35
|
+
Provides-Extra: anthropic
|
|
36
|
+
Requires-Dist: langchain-anthropic>=1.4; extra == "anthropic"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# aec-agent-core
|
|
40
|
+
|
|
41
|
+
The shared core of LLM agents that review and produce architecture,
|
|
42
|
+
engineering and construction (AEC) documents. It holds what an agent runs
|
|
43
|
+
below its chat page: the model connections, the vision calls and the file
|
|
44
|
+
tools beside them, the figure helper with its plotting box, the agent's
|
|
45
|
+
guards, limits and kept work, Word and Excel in and out, the run record and
|
|
46
|
+
its checks, and the switches. It has no user interface of its own.
|
|
47
|
+
|
|
48
|
+
Two applications are built on it:
|
|
49
|
+
|
|
50
|
+
- **GeotechStaffEngineer** (`pip install geotech-staff-engineer`), the
|
|
51
|
+
geotechnical engineering agent, which pins this package exactly;
|
|
52
|
+
- a document review application.
|
|
53
|
+
|
|
54
|
+
## Status
|
|
55
|
+
|
|
56
|
+
**1.0.0.dev0: an empty package.** It exists so that its build, its tests and
|
|
57
|
+
the applications' pins on it can be checked before any code moves in. The
|
|
58
|
+
only thing in it today is `aec_agent_core.install_aliases`, which lets a
|
|
59
|
+
module keep answering to its old import path after it moves here, as the
|
|
60
|
+
same module object (so an import or a test patch by either name reaches the
|
|
61
|
+
one copy of the code).
|
|
62
|
+
|
|
63
|
+
It is not yet published; GeotechStaffEngineer installs it from its own
|
|
64
|
+
repository (`pip install -e packages/aec-agent-core`) until it is.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
pip install aec-agent-core # once published
|
|
70
|
+
pip install "aec-agent-core[anthropic]" # with Claude through LangChain
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Licence
|
|
74
|
+
|
|
75
|
+
MIT, as GeotechStaffEngineer. See `LICENSE`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# aec-agent-core
|
|
2
|
+
|
|
3
|
+
The shared core of LLM agents that review and produce architecture,
|
|
4
|
+
engineering and construction (AEC) documents. It holds what an agent runs
|
|
5
|
+
below its chat page: the model connections, the vision calls and the file
|
|
6
|
+
tools beside them, the figure helper with its plotting box, the agent's
|
|
7
|
+
guards, limits and kept work, Word and Excel in and out, the run record and
|
|
8
|
+
its checks, and the switches. It has no user interface of its own.
|
|
9
|
+
|
|
10
|
+
Two applications are built on it:
|
|
11
|
+
|
|
12
|
+
- **GeotechStaffEngineer** (`pip install geotech-staff-engineer`), the
|
|
13
|
+
geotechnical engineering agent, which pins this package exactly;
|
|
14
|
+
- a document review application.
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
**1.0.0.dev0: an empty package.** It exists so that its build, its tests and
|
|
19
|
+
the applications' pins on it can be checked before any code moves in. The
|
|
20
|
+
only thing in it today is `aec_agent_core.install_aliases`, which lets a
|
|
21
|
+
module keep answering to its old import path after it moves here, as the
|
|
22
|
+
same module object (so an import or a test patch by either name reaches the
|
|
23
|
+
one copy of the code).
|
|
24
|
+
|
|
25
|
+
It is not yet published; GeotechStaffEngineer installs it from its own
|
|
26
|
+
repository (`pip install -e packages/aec-agent-core`) until it is.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install aec-agent-core # once published
|
|
32
|
+
pip install "aec-agent-core[anthropic]" # with Claude through LangChain
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Licence
|
|
36
|
+
|
|
37
|
+
MIT, as GeotechStaffEngineer. See `LICENSE`.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""aec-agent-core: what document agents run below their chat pages.
|
|
2
|
+
|
|
3
|
+
1.0.0.dev0 is the empty package of the app split's step 10 (BACKLOG INF-32;
|
|
4
|
+
``module_work/APP_SPLIT_PLAN.md``). It holds one thing, the alias helper
|
|
5
|
+
step 11 uses when it moves a module in here, so the module's old import
|
|
6
|
+
path keeps working until step 16 takes the aliases out:
|
|
7
|
+
|
|
8
|
+
# in the old package's __init__.py, e.g. funhouse_agent/__init__.py
|
|
9
|
+
from aec_agent_core import install_aliases
|
|
10
|
+
install_aliases({"funhouse_agent.vision_tools": "aec_agent_core.vision_tools"})
|
|
11
|
+
|
|
12
|
+
From then on ``import funhouse_agent.vision_tools`` (or ``from
|
|
13
|
+
funhouse_agent import vision_tools``) gives the very module object that
|
|
14
|
+
``aec_agent_core.vision_tools`` is, with no stub file left at the old path.
|
|
15
|
+
So git follows the move as a rename, and a test that patches by the old
|
|
16
|
+
name (``monkeypatch.setattr("funhouse_agent.vision_tools.x", ...)``)
|
|
17
|
+
patches the real module, which a re-exporting stub would silently not do.
|
|
18
|
+
|
|
19
|
+
An alias also covers what lies under the name: with the table above,
|
|
20
|
+
``funhouse_agent.vision_tools.sub`` is ``aec_agent_core.vision_tools.sub``.
|
|
21
|
+
|
|
22
|
+
The alias is LAZY: nothing is imported until the old name is looked up (an
|
|
23
|
+
import, or ``importlib.util.find_spec``, which imports the target). The module
|
|
24
|
+
keeps its own ``__name__``, ``__spec__`` and ``__file__`` (the new ones).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import importlib
|
|
30
|
+
import importlib.abc
|
|
31
|
+
import importlib.machinery
|
|
32
|
+
import sys
|
|
33
|
+
import threading
|
|
34
|
+
from typing import Mapping
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0.dev0"
|
|
37
|
+
|
|
38
|
+
__all__ = ["install_aliases", "remove_aliases", "aliases", "resolve_alias",
|
|
39
|
+
"__version__"]
|
|
40
|
+
|
|
41
|
+
_LOCK = threading.Lock()
|
|
42
|
+
#: old dotted name -> new dotted name, for every installed alias.
|
|
43
|
+
_TABLE: dict[str, str] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _valid_name(name: object) -> bool:
|
|
47
|
+
return (isinstance(name, str) and bool(name)
|
|
48
|
+
and all(part.isidentifier() for part in name.split(".")))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_alias(fullname: str) -> str | None:
|
|
52
|
+
"""The new name an old dotted name stands for, or None if none does.
|
|
53
|
+
|
|
54
|
+
The longest matching alias wins, so ``a.b`` -> ``x`` and ``a.b.c`` ->
|
|
55
|
+
``y`` send ``a.b.c.d`` to ``y.d``.
|
|
56
|
+
"""
|
|
57
|
+
with _LOCK:
|
|
58
|
+
table = dict(_TABLE)
|
|
59
|
+
name = fullname
|
|
60
|
+
rest: list[str] = []
|
|
61
|
+
while name:
|
|
62
|
+
new = table.get(name)
|
|
63
|
+
if new is not None:
|
|
64
|
+
return ".".join([new, *reversed(rest)])
|
|
65
|
+
name, _, last = name.rpartition(".")
|
|
66
|
+
rest.append(last)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _AliasLoader(importlib.abc.Loader):
|
|
71
|
+
"""Hands the import system the already-imported real module."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, target: str):
|
|
74
|
+
self._target = target
|
|
75
|
+
|
|
76
|
+
def create_module(self, spec):
|
|
77
|
+
return importlib.import_module(self._target)
|
|
78
|
+
|
|
79
|
+
def exec_module(self, module):
|
|
80
|
+
# The import system has just set module.__spec__ to the ALIAS spec
|
|
81
|
+
# (it always does, even on an existing module); put the module's own
|
|
82
|
+
# back, so reload, pickle by reference and find_spec see the real
|
|
83
|
+
# module. Nothing else on the module was overwritten: __name__,
|
|
84
|
+
# __loader__, __package__ and __file__ were already set.
|
|
85
|
+
spec = _REAL_SPECS.get(self._target)
|
|
86
|
+
if spec is not None and module is sys.modules.get(self._target):
|
|
87
|
+
module.__spec__ = spec
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
#: The real spec of every module an alias handed out, kept before the import
|
|
91
|
+
#: system overwrote it (see _AliasLoader.exec_module).
|
|
92
|
+
_REAL_SPECS: dict[str, object] = {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _AliasFinder(importlib.abc.MetaPathFinder):
|
|
96
|
+
"""Finds an old (aliased) name and answers with the new module."""
|
|
97
|
+
|
|
98
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
99
|
+
new = resolve_alias(fullname)
|
|
100
|
+
if new is None or new == fullname:
|
|
101
|
+
return None
|
|
102
|
+
module = importlib.import_module(new)
|
|
103
|
+
_REAL_SPECS.setdefault(new, module.__spec__)
|
|
104
|
+
is_pkg = hasattr(module, "__path__")
|
|
105
|
+
return importlib.machinery.ModuleSpec(
|
|
106
|
+
fullname, _AliasLoader(new), is_package=is_pkg)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_FINDER = _AliasFinder()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _ensure_finder() -> None:
|
|
113
|
+
# First in line, so a stale file left at an old path cannot shadow the
|
|
114
|
+
# moved module.
|
|
115
|
+
if _FINDER not in sys.meta_path:
|
|
116
|
+
sys.meta_path.insert(0, _FINDER)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def install_aliases(table: Mapping[str, str]) -> None:
|
|
120
|
+
"""Make each old dotted name import as the module of its new name.
|
|
121
|
+
|
|
122
|
+
``table`` maps old name -> new name, both full dotted module names.
|
|
123
|
+
Installing the same pair twice is harmless; pointing an old name at a
|
|
124
|
+
different new name, aliasing a name to itself, or aliasing a name that
|
|
125
|
+
is already imported as a DIFFERENT module raises ``ValueError``, because
|
|
126
|
+
each would leave two copies of the code or an alias that never applies.
|
|
127
|
+
"""
|
|
128
|
+
pairs = dict(table)
|
|
129
|
+
for old, new in pairs.items():
|
|
130
|
+
if not (_valid_name(old) and _valid_name(new)):
|
|
131
|
+
raise ValueError(f"not a dotted module name: {old!r} -> {new!r}")
|
|
132
|
+
if old == new or new.startswith(old + ".") or old.startswith(new + "."):
|
|
133
|
+
raise ValueError(f"an alias cannot point into itself: {old} -> {new}")
|
|
134
|
+
with _LOCK:
|
|
135
|
+
for old, new in pairs.items():
|
|
136
|
+
have = _TABLE.get(old)
|
|
137
|
+
if have is not None and have != new:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"{old} is already an alias of {have}, not {new}")
|
|
140
|
+
loaded = sys.modules.get(old)
|
|
141
|
+
if loaded is not None and loaded is not sys.modules.get(new):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"{old} is already imported as its own module; an alias "
|
|
144
|
+
f"to {new} would leave two copies")
|
|
145
|
+
_TABLE.update(pairs)
|
|
146
|
+
_ensure_finder()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def remove_aliases(*olds: str) -> None:
|
|
150
|
+
"""Take aliases out (all of them when no name is given).
|
|
151
|
+
|
|
152
|
+
A module already imported by an old name stays in ``sys.modules`` under
|
|
153
|
+
it; this only stops new imports from resolving. For tests, and for
|
|
154
|
+
step 16.
|
|
155
|
+
"""
|
|
156
|
+
with _LOCK:
|
|
157
|
+
for old in (olds or tuple(_TABLE)):
|
|
158
|
+
_TABLE.pop(old, None)
|
|
159
|
+
if not _TABLE and _FINDER in sys.meta_path:
|
|
160
|
+
sys.meta_path.remove(_FINDER)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def aliases() -> dict[str, str]:
|
|
164
|
+
"""A copy of the installed aliases, old name -> new name."""
|
|
165
|
+
with _LOCK:
|
|
166
|
+
return dict(_TABLE)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aec-agent-core
|
|
3
|
+
Version: 1.0.0.dev0
|
|
4
|
+
Summary: The shared core of LLM agents that review and produce architecture, engineering and construction documents: model connections, vision calls, guards and records, Word and Excel, and a figure helper. No user interface of its own.
|
|
5
|
+
Author-email: Sean O'Connell <soconnell345@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/soconnell345-geotech/GeotechStaffEngineer
|
|
8
|
+
Project-URL: Repository, https://github.com/soconnell345-geotech/GeotechStaffEngineer
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.26
|
|
22
|
+
Requires-Dist: scipy>=1.13
|
|
23
|
+
Requires-Dist: deepagents<0.7.25,>=0.6.8
|
|
24
|
+
Requires-Dist: langchain<1.4.6,>=1.3
|
|
25
|
+
Requires-Dist: langgraph<1.3,>=1.2
|
|
26
|
+
Requires-Dist: typing_extensions>=4.13
|
|
27
|
+
Requires-Dist: openai<3.29.1
|
|
28
|
+
Requires-Dist: langchain-openai>=1.3
|
|
29
|
+
Requires-Dist: matplotlib>=3.8
|
|
30
|
+
Requires-Dist: plotly>=5
|
|
31
|
+
Requires-Dist: python-docx>=1.1
|
|
32
|
+
Requires-Dist: markdown-it-py>=3
|
|
33
|
+
Requires-Dist: openpyxl>=3.1
|
|
34
|
+
Requires-Dist: planlens>=0.14
|
|
35
|
+
Provides-Extra: anthropic
|
|
36
|
+
Requires-Dist: langchain-anthropic>=1.4; extra == "anthropic"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# aec-agent-core
|
|
40
|
+
|
|
41
|
+
The shared core of LLM agents that review and produce architecture,
|
|
42
|
+
engineering and construction (AEC) documents. It holds what an agent runs
|
|
43
|
+
below its chat page: the model connections, the vision calls and the file
|
|
44
|
+
tools beside them, the figure helper with its plotting box, the agent's
|
|
45
|
+
guards, limits and kept work, Word and Excel in and out, the run record and
|
|
46
|
+
its checks, and the switches. It has no user interface of its own.
|
|
47
|
+
|
|
48
|
+
Two applications are built on it:
|
|
49
|
+
|
|
50
|
+
- **GeotechStaffEngineer** (`pip install geotech-staff-engineer`), the
|
|
51
|
+
geotechnical engineering agent, which pins this package exactly;
|
|
52
|
+
- a document review application.
|
|
53
|
+
|
|
54
|
+
## Status
|
|
55
|
+
|
|
56
|
+
**1.0.0.dev0: an empty package.** It exists so that its build, its tests and
|
|
57
|
+
the applications' pins on it can be checked before any code moves in. The
|
|
58
|
+
only thing in it today is `aec_agent_core.install_aliases`, which lets a
|
|
59
|
+
module keep answering to its old import path after it moves here, as the
|
|
60
|
+
same module object (so an import or a test patch by either name reaches the
|
|
61
|
+
one copy of the code).
|
|
62
|
+
|
|
63
|
+
It is not yet published; GeotechStaffEngineer installs it from its own
|
|
64
|
+
repository (`pip install -e packages/aec-agent-core`) until it is.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
pip install aec-agent-core # once published
|
|
70
|
+
pip install "aec-agent-core[anthropic]" # with Claude through LangChain
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Licence
|
|
74
|
+
|
|
75
|
+
MIT, as GeotechStaffEngineer. See `LICENSE`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aec_agent_core/__init__.py
|
|
5
|
+
aec_agent_core.egg-info/PKG-INFO
|
|
6
|
+
aec_agent_core.egg-info/SOURCES.txt
|
|
7
|
+
aec_agent_core.egg-info/dependency_links.txt
|
|
8
|
+
aec_agent_core.egg-info/requires.txt
|
|
9
|
+
aec_agent_core.egg-info/top_level.txt
|
|
10
|
+
tests/test_core_aliases.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
numpy>=1.26
|
|
2
|
+
scipy>=1.13
|
|
3
|
+
deepagents<0.7.25,>=0.6.8
|
|
4
|
+
langchain<1.4.6,>=1.3
|
|
5
|
+
langgraph<1.3,>=1.2
|
|
6
|
+
typing_extensions>=4.13
|
|
7
|
+
openai<3.29.1
|
|
8
|
+
langchain-openai>=1.3
|
|
9
|
+
matplotlib>=3.8
|
|
10
|
+
plotly>=5
|
|
11
|
+
python-docx>=1.1
|
|
12
|
+
markdown-it-py>=3
|
|
13
|
+
openpyxl>=3.1
|
|
14
|
+
planlens>=0.14
|
|
15
|
+
|
|
16
|
+
[anthropic]
|
|
17
|
+
langchain-anthropic>=1.4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aec_agent_core
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
# The core both agents run below their chat pages (the app split, BACKLOG
|
|
6
|
+
# INF-26 / INF-32; plan module_work/APP_SPLIT_PLAN.md sections 1.1 and 2.3).
|
|
7
|
+
# Step 10 (this file): the package exists, EMPTY apart from the alias helper,
|
|
8
|
+
# with its build, CI and a wheel test. Step 11 moves the code in. Step 12
|
|
9
|
+
# publishes it, on the owner's word; until then nothing publishes it (the
|
|
10
|
+
# publish workflow builds the repository root only).
|
|
11
|
+
[project]
|
|
12
|
+
name = "aec-agent-core"
|
|
13
|
+
# Kept equal to geotech-staff-engineer's exact pin on it (its pyproject) and
|
|
14
|
+
# to aec_agent_core.__version__; webapp/tests/test_wheels.py checks all three.
|
|
15
|
+
version = "1.0.0.dev0"
|
|
16
|
+
description = "The shared core of LLM agents that review and produce architecture, engineering and construction documents: model connections, vision calls, guards and records, Word and Excel, and a figure helper. No user interface of its own."
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICENSE"]
|
|
20
|
+
requires-python = ">=3.10"
|
|
21
|
+
authors = [
|
|
22
|
+
{name = "Sean O'Connell", email = "soconnell345@gmail.com"},
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Topic :: Scientific/Engineering",
|
|
34
|
+
]
|
|
35
|
+
# The list of plan section 1.1, at the caps and floors geotech-staff-engineer
|
|
36
|
+
# has today (its pyproject carries the reasons for each; keep the two in
|
|
37
|
+
# step: webapp/tests/test_version_guard.py reads the agent-stack caps in both
|
|
38
|
+
# files). Step 10 installs nothing new: every line here is already a core
|
|
39
|
+
# requirement of geotech-staff-engineer.
|
|
40
|
+
dependencies = [
|
|
41
|
+
# numpy 1.26, not 2.0 (INF-53): Foundry's transforms repos hold 1.26.4.
|
|
42
|
+
"numpy>=1.26",
|
|
43
|
+
# The plotting box lets the model's code import scipy.
|
|
44
|
+
"scipy>=1.13",
|
|
45
|
+
# The agent stack, capped at the versions the offline gate exercised
|
|
46
|
+
# (INF-03, 2026-10-27).
|
|
47
|
+
"deepagents>=0.6.8,<0.7.25",
|
|
48
|
+
"langchain>=1.3,<1.4.6",
|
|
49
|
+
"langgraph>=1.2,<1.3",
|
|
50
|
+
# langchain-protocol needs typing_extensions.TypedDict(extra_items=...)
|
|
51
|
+
# (PEP 728), 4.13 or later; some hosts ship an older one.
|
|
52
|
+
"typing_extensions>=4.13",
|
|
53
|
+
# openai 3.x builds its own client on httpx2; 3.29.0 is the highest tested.
|
|
54
|
+
"openai<3.29.1",
|
|
55
|
+
"langchain-openai>=1.3",
|
|
56
|
+
# The figure helper, the plotting box and the chart stack.
|
|
57
|
+
"matplotlib>=3.8",
|
|
58
|
+
"plotly>=5",
|
|
59
|
+
# Word out (and in), Excel out and in.
|
|
60
|
+
"python-docx>=1.1",
|
|
61
|
+
"markdown-it-py>=3",
|
|
62
|
+
"openpyxl>=3.1",
|
|
63
|
+
# The document layer; the core binds its review tools.
|
|
64
|
+
"planlens>=0.14",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[project.optional-dependencies]
|
|
68
|
+
# Claude through LangChain. GSE asks for it; a GPT-only host need not.
|
|
69
|
+
anthropic = ["langchain-anthropic>=1.4"]
|
|
70
|
+
|
|
71
|
+
[project.urls]
|
|
72
|
+
Homepage = "https://github.com/soconnell345-geotech/GeotechStaffEngineer"
|
|
73
|
+
Repository = "https://github.com/soconnell345-geotech/GeotechStaffEngineer"
|
|
74
|
+
|
|
75
|
+
[tool.setuptools.packages.find]
|
|
76
|
+
include = ["aec_agent_core*"]
|
|
77
|
+
exclude = ["*.tests", "*.tests.*", "tests*"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""The alias helper step 11 uses to keep old import paths working
|
|
2
|
+
(``aec_agent_core.install_aliases``; plan ``module_work/APP_SPLIT_PLAN.md``,
|
|
3
|
+
section 4, "Moves that git can follow").
|
|
4
|
+
|
|
5
|
+
Each test builds a throwaway pair of packages on disk: ``old_<n>`` (the
|
|
6
|
+
package a module moved out of) and ``new_<n>`` (the one it moved into).
|
|
7
|
+
|
|
8
|
+
The alias table is process-wide, and from step 11 the apps install their
|
|
9
|
+
own tables before these tests run. So a test removes only the aliases it
|
|
10
|
+
added and checks only its own entries, never that the table is exactly its
|
|
11
|
+
own.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
import importlib.util
|
|
18
|
+
import itertools
|
|
19
|
+
import pickle
|
|
20
|
+
import sys
|
|
21
|
+
import textwrap
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
import aec_agent_core as core
|
|
26
|
+
|
|
27
|
+
_COUNTER = itertools.count()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def pair(tmp_path, monkeypatch):
|
|
32
|
+
"""(old, new) package names on sys.path; cleaned up afterwards."""
|
|
33
|
+
n = next(_COUNTER)
|
|
34
|
+
old, new = f"old_pkg_{n}", f"new_pkg_{n}"
|
|
35
|
+
|
|
36
|
+
def write(rel, text=""):
|
|
37
|
+
path = tmp_path / rel
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
path.write_text(textwrap.dedent(text), encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
write(f"{old}/__init__.py")
|
|
42
|
+
write(f"{new}/__init__.py")
|
|
43
|
+
write(f"{new}/tool.py", """
|
|
44
|
+
VALUE = 1
|
|
45
|
+
|
|
46
|
+
def answer():
|
|
47
|
+
return VALUE
|
|
48
|
+
|
|
49
|
+
def made_here():
|
|
50
|
+
return 42
|
|
51
|
+
""")
|
|
52
|
+
write(f"{new}/box/__init__.py", "NAME = 'box'\n")
|
|
53
|
+
write(f"{new}/box/child.py", "WHO = 'child'\n")
|
|
54
|
+
monkeypatch.syspath_prepend(str(tmp_path))
|
|
55
|
+
importlib.invalidate_caches()
|
|
56
|
+
before = core.aliases()
|
|
57
|
+
yield old, new, write
|
|
58
|
+
added = [name for name in core.aliases() if name not in before]
|
|
59
|
+
if added: # remove_aliases() with no name removes ALL
|
|
60
|
+
core.remove_aliases(*added)
|
|
61
|
+
for name in list(sys.modules):
|
|
62
|
+
if name.split(".")[0] in (old, new):
|
|
63
|
+
del sys.modules[name]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_the_old_name_is_the_same_module_object(pair):
|
|
67
|
+
old, new, _ = pair
|
|
68
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
69
|
+
moved = importlib.import_module(f"{old}.tool")
|
|
70
|
+
real = importlib.import_module(f"{new}.tool")
|
|
71
|
+
assert moved is real
|
|
72
|
+
assert sys.modules[f"{old}.tool"] is real
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_from_import_and_the_parent_attribute(pair):
|
|
76
|
+
old, new, _ = pair
|
|
77
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
78
|
+
exec(f"from {old} import tool as t1\nimport {old}.tool as t2\n"
|
|
79
|
+
f"assert t1 is t2", {})
|
|
80
|
+
parent = sys.modules[old]
|
|
81
|
+
assert parent.tool is sys.modules[f"{new}.tool"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_the_module_keeps_its_own_name_spec_and_file(pair):
|
|
85
|
+
old, new, _ = pair
|
|
86
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
87
|
+
mod = importlib.import_module(f"{old}.tool")
|
|
88
|
+
assert mod.__name__ == f"{new}.tool"
|
|
89
|
+
assert mod.__spec__.name == f"{new}.tool"
|
|
90
|
+
assert mod.__file__.endswith("tool.py") and new in mod.__file__
|
|
91
|
+
# so reload still works on the real module
|
|
92
|
+
assert importlib.reload(mod) is mod
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_a_patch_by_the_old_name_reaches_the_real_module(pair, monkeypatch):
|
|
96
|
+
old, new, _ = pair
|
|
97
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
98
|
+
monkeypatch.setattr(f"{old}.tool.VALUE", 7)
|
|
99
|
+
real = importlib.import_module(f"{new}.tool")
|
|
100
|
+
assert real.answer() == 7
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_install_is_lazy(pair):
|
|
104
|
+
old, new, _ = pair
|
|
105
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
106
|
+
assert f"{new}.tool" not in sys.modules
|
|
107
|
+
assert f"{old}.tool" not in sys.modules
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_a_package_alias_covers_what_lies_under_it(pair):
|
|
111
|
+
old, new, _ = pair
|
|
112
|
+
core.install_aliases({f"{old}.box": f"{new}.box"})
|
|
113
|
+
child = importlib.import_module(f"{old}.box.child")
|
|
114
|
+
assert child is importlib.import_module(f"{new}.box.child")
|
|
115
|
+
assert importlib.import_module(f"{old}.box").NAME == "box"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_the_longest_alias_wins(pair):
|
|
119
|
+
old, new, _ = pair
|
|
120
|
+
core.install_aliases({f"{old}.box": f"{new}.box",
|
|
121
|
+
f"{old}.box.other": f"{new}.tool"})
|
|
122
|
+
assert core.resolve_alias(f"{old}.box.other") == f"{new}.tool"
|
|
123
|
+
assert core.resolve_alias(f"{old}.box.child") == f"{new}.box.child"
|
|
124
|
+
assert core.resolve_alias(f"{old}.nothing") is None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_find_spec_by_the_old_name(pair):
|
|
128
|
+
old, new, _ = pair
|
|
129
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
130
|
+
spec = importlib.util.find_spec(f"{old}.tool")
|
|
131
|
+
assert spec is not None and spec.name == f"{old}.tool"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_a_stale_file_at_the_old_path_does_not_shadow_the_move(pair):
|
|
135
|
+
old, new, write = pair
|
|
136
|
+
write(f"{old}/tool.py", "STALE = True\n")
|
|
137
|
+
importlib.invalidate_caches()
|
|
138
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
139
|
+
mod = importlib.import_module(f"{old}.tool")
|
|
140
|
+
assert not hasattr(mod, "STALE")
|
|
141
|
+
assert mod is importlib.import_module(f"{new}.tool")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_pickle_by_reference_uses_the_real_name(pair):
|
|
145
|
+
old, new, _ = pair
|
|
146
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
147
|
+
fn = importlib.import_module(f"{old}.tool").made_here
|
|
148
|
+
assert pickle.loads(pickle.dumps(fn)) is fn
|
|
149
|
+
assert new.encode() in pickle.dumps(fn)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_installing_the_same_pair_twice_is_harmless(pair):
|
|
153
|
+
old, new, _ = pair
|
|
154
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
155
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
156
|
+
assert core.aliases()[f"{old}.tool"] == f"{new}.tool"
|
|
157
|
+
assert sys.meta_path.count(core._FINDER) == 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize("table", [
|
|
161
|
+
{"a.b": "a.b"},
|
|
162
|
+
{"a.b": "a.b.c"},
|
|
163
|
+
{"a.b.c": "a.b"},
|
|
164
|
+
{"a..b": "x"},
|
|
165
|
+
{"a.b": ""},
|
|
166
|
+
{"a-b.c": "x.y"},
|
|
167
|
+
])
|
|
168
|
+
def test_aliases_that_could_never_apply_are_refused(table):
|
|
169
|
+
before = core.aliases()
|
|
170
|
+
with pytest.raises(ValueError):
|
|
171
|
+
core.install_aliases(table)
|
|
172
|
+
after = core.aliases()
|
|
173
|
+
assert all(after.get(old) == before.get(old) for old in table)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_repointing_an_alias_is_refused(pair):
|
|
177
|
+
old, new, _ = pair
|
|
178
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
179
|
+
with pytest.raises(ValueError, match="already an alias"):
|
|
180
|
+
core.install_aliases({f"{old}.tool": f"{new}.box"})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_an_old_name_already_imported_as_its_own_module_is_refused(pair):
|
|
184
|
+
old, new, write = pair
|
|
185
|
+
write(f"{old}/tool.py", "OWN = True\n")
|
|
186
|
+
importlib.invalidate_caches()
|
|
187
|
+
importlib.import_module(f"{old}.tool")
|
|
188
|
+
with pytest.raises(ValueError, match="two copies"):
|
|
189
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_remove_aliases_stops_new_imports(pair):
|
|
193
|
+
old, new, _ = pair
|
|
194
|
+
core.install_aliases({f"{old}.tool": f"{new}.tool"})
|
|
195
|
+
core.remove_aliases(f"{old}.tool")
|
|
196
|
+
assert f"{old}.tool" not in core.aliases()
|
|
197
|
+
# The finder leaves sys.meta_path with the last alias, and stays while
|
|
198
|
+
# any other alias (an app's table) is still installed.
|
|
199
|
+
if core.aliases():
|
|
200
|
+
assert core._FINDER in sys.meta_path
|
|
201
|
+
else:
|
|
202
|
+
assert core._FINDER not in sys.meta_path
|
|
203
|
+
with pytest.raises(ModuleNotFoundError):
|
|
204
|
+
importlib.import_module(f"{old}.tool")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_a_missing_target_is_an_import_error(pair):
|
|
208
|
+
old, new, _ = pair
|
|
209
|
+
core.install_aliases({f"{old}.gone": f"{new}.gone"})
|
|
210
|
+
with pytest.raises(ModuleNotFoundError):
|
|
211
|
+
importlib.import_module(f"{old}.gone")
|
|
212
|
+
|