aec-agent-core 1.0.0.dev0__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,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,6 @@
1
+ aec_agent_core/__init__.py,sha256=UD8OKewavWa9LjQsZDexoujf0bK84bYmJbBQk9PKFGc,6288
2
+ aec_agent_core-1.0.0.dev0.dist-info/licenses/LICENSE,sha256=tUQ30KFGkSexrpVCJHr-ZBiREZfviUU6-IfDS0msr8g,1076
3
+ aec_agent_core-1.0.0.dev0.dist-info/METADATA,sha256=zWi5slgJeR4jUh8HBJsa6_w7pLJZbNBykFJA-JhDOgI,3053
4
+ aec_agent_core-1.0.0.dev0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ aec_agent_core-1.0.0.dev0.dist-info/top_level.txt,sha256=mc6IEpnX_M06db-9bQHx7Mtbp6yB02pdXRiDI5WMQU8,15
6
+ aec_agent_core-1.0.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ aec_agent_core