agent2learn 0.1.2__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.
Files changed (46) hide show
  1. agent2learn/__init__.py +3 -0
  2. agent2learn/_release.py +19 -0
  3. agent2learn/aipolicy.py +182 -0
  4. agent2learn/api.py +590 -0
  5. agent2learn/audit.py +358 -0
  6. agent2learn/auth/__init__.py +282 -0
  7. agent2learn/auth/cdp.py +1067 -0
  8. agent2learn/auth/paste.py +378 -0
  9. agent2learn/calendar.py +525 -0
  10. agent2learn/calibrate.py +347 -0
  11. agent2learn/check.py +1091 -0
  12. agent2learn/cli.py +2039 -0
  13. agent2learn/clock.py +39 -0
  14. agent2learn/config.py +205 -0
  15. agent2learn/console.py +229 -0
  16. agent2learn/convert.py +1223 -0
  17. agent2learn/doctor.py +1167 -0
  18. agent2learn/errors.py +32 -0
  19. agent2learn/ground.py +735 -0
  20. agent2learn/index.py +614 -0
  21. agent2learn/ingest.py +3229 -0
  22. agent2learn/locations.py +247 -0
  23. agent2learn/outlines.py +754 -0
  24. agent2learn/paths.py +683 -0
  25. agent2learn/pipeline.py +392 -0
  26. agent2learn/privacy.py +1123 -0
  27. agent2learn/schools/__init__.py +29 -0
  28. agent2learn/schools/_base.py +194 -0
  29. agent2learn/schools/generic.py +78 -0
  30. agent2learn/schools/uwaterloo.py +66 -0
  31. agent2learn/session.py +373 -0
  32. agent2learn/skills.py +1081 -0
  33. agent2learn/snapshot.py +399 -0
  34. agent2learn/submit.py +1047 -0
  35. agent2learn/transactions.py +157 -0
  36. agent2learn/upgrade.py +288 -0
  37. agent2learn/vault.py +1134 -0
  38. agent2learn-0.1.2.data/data/a2l-coursework/SKILL.md +52 -0
  39. agent2learn-0.1.2.data/data/a2l-setup/SKILL.md +27 -0
  40. agent2learn-0.1.2.data/data/a2l-study/SKILL.md +27 -0
  41. agent2learn-0.1.2.data/data/a2l-sync/SKILL.md +30 -0
  42. agent2learn-0.1.2.dist-info/METADATA +186 -0
  43. agent2learn-0.1.2.dist-info/RECORD +46 -0
  44. agent2learn-0.1.2.dist-info/WHEEL +4 -0
  45. agent2learn-0.1.2.dist-info/entry_points.txt +3 -0
  46. agent2learn-0.1.2.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,3 @@
1
+ """Agent2Learn package."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,19 @@
1
+ """Build-time release switches.
2
+
3
+ These are constants a release engineer sets deliberately, not user configuration and not
4
+ something a test may reach around. ``submit.py`` reads :data:`SUBMISSION_AVAILABLE` through
5
+ ``submit.release_capability()``; tests exercise the upload path by passing an explicit
6
+ ``SubmissionCapability`` instead, so the production check is never monkeypatched away.
7
+
8
+ ``SUBMISSION_AVAILABLE`` stays ``False`` until a supervised, designated non-graded upload has
9
+ passed against a real instance for that exact release candidate. If the gate cannot pass, the
10
+ published build is rebuilt with the mutating path disabled and every artifact test is rerun.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Final
16
+
17
+ SUBMISSION_AVAILABLE: Final[bool] = False
18
+
19
+ __all__ = ["SUBMISSION_AVAILABLE"]
@@ -0,0 +1,182 @@
1
+ # ruff: noqa: E501
2
+ """Informational AI-policy surfacing from already-rendered local outlines."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+
12
+ from agent2learn import paths
13
+
14
+ AI_POLICY_SCHEMA_VERSION = 1
15
+ _KEYWORDS = re.compile(
16
+ r"generative ai|chatgpt|artificial intelligence|genai|large language model", re.IGNORECASE
17
+ )
18
+ _HEADING = re.compile(r"^\s{0,3}#{1,6}\s+")
19
+ _POLICY_START = "<!-- a2l:ai-policy:start -->"
20
+ _POLICY_END = "<!-- a2l:ai-policy:end -->"
21
+
22
+
23
+ def surface_ai_policy(
24
+ course_dir: Path, outline: Path | None, *, root: Path | None = None
25
+ ) -> dict[str, object]:
26
+ """Record a local observation without classifying, scoring, or enforcing a policy."""
27
+ record = _scan_outline(course_dir, outline)
28
+ _write_record(course_dir, record, root=root)
29
+ return record
30
+
31
+
32
+ def _scan_outline(course_dir: Path, outline: Path | None) -> dict[str, object]:
33
+ if outline is None:
34
+ return _unavailable()
35
+ try:
36
+ with open(os.fspath(paths.long_path(outline)), encoding="utf-8", newline="") as handle:
37
+ lines = handle.read().splitlines()
38
+ except (FileNotFoundError, OSError, UnicodeError):
39
+ return _unavailable()
40
+
41
+ heading_match = next(
42
+ (
43
+ (index, line)
44
+ for index, line in enumerate(lines)
45
+ if _HEADING.match(line) and _KEYWORDS.search(line)
46
+ ),
47
+ None,
48
+ )
49
+ if heading_match is not None:
50
+ start, _ = heading_match
51
+ end = next(
52
+ (index for index in range(start + 1, len(lines)) if _HEADING.match(lines[index])),
53
+ len(lines),
54
+ )
55
+ text = "\n".join(lines[start:end]).strip()
56
+ return _found(course_dir, outline, text, start)
57
+
58
+ for offset, block in _paragraph_blocks(lines):
59
+ if _KEYWORDS.search("\n".join(block)):
60
+ return _found(course_dir, outline, "\n".join(block).strip(), offset)
61
+ return {
62
+ "schema_version": AI_POLICY_SCHEMA_VERSION,
63
+ "status": "not_found_in_scanned_outline",
64
+ "text": None,
65
+ "source": None,
66
+ }
67
+
68
+
69
+ def _paragraph_blocks(lines: Sequence[str]) -> list[tuple[int, list[str]]]:
70
+ blocks: list[tuple[int, list[str]]] = []
71
+ current: list[str] = []
72
+ start = 0
73
+ for index, line in enumerate(lines):
74
+ if line.strip():
75
+ if not current:
76
+ start = index
77
+ current.append(line)
78
+ elif current:
79
+ blocks.append((start, current))
80
+ current = []
81
+ if current:
82
+ blocks.append((start, current))
83
+ return blocks
84
+
85
+
86
+ def _found(course_dir: Path, outline: Path, text: str, line: int) -> dict[str, object]:
87
+ try:
88
+ source = f"{outline.relative_to(course_dir).as_posix()}:{line + 1}"
89
+ except ValueError:
90
+ source = f"{outline.name}:{line + 1}"
91
+ return {
92
+ "schema_version": AI_POLICY_SCHEMA_VERSION,
93
+ "status": "found",
94
+ "text": text,
95
+ "source": source,
96
+ }
97
+
98
+
99
+ def _unavailable() -> dict[str, object]:
100
+ return {
101
+ "schema_version": AI_POLICY_SCHEMA_VERSION,
102
+ "status": "outline_unavailable",
103
+ "text": None,
104
+ "source": None,
105
+ }
106
+
107
+
108
+ def _write_record(course_dir: Path, record: dict[str, object], *, root: Path | None = None) -> None:
109
+ destination = course_dir / "_meta" / "ai_policy.json"
110
+ paths.ensure_dir(destination.parent, root=root)
111
+ paths.atomic_write_text(
112
+ destination,
113
+ json.dumps(record, ensure_ascii=False, sort_keys=True, indent=2, separators=(",", ": "))
114
+ + "\n",
115
+ root=root,
116
+ )
117
+ _surface_index_line(course_dir, record, root=root)
118
+
119
+
120
+ def surface_course_ai_policy(
121
+ course_dir: Path, outlines: Sequence[Path], *, root: Path | None = None
122
+ ) -> dict[str, object]:
123
+ """Record the first found clause across successful local outline renders.
124
+
125
+ All supplied paths were successfully rendered by the outline boundary. An empty set is
126
+ therefore unavailable coverage; a non-empty set with no keyword is a scanned no-match.
127
+ """
128
+ if not outlines:
129
+ return surface_ai_policy(course_dir, None, root=root)
130
+ last: dict[str, object] | None = None
131
+ for outline in sorted(outlines, key=lambda value: value.as_posix()):
132
+ record = _scan_outline(course_dir, outline)
133
+ if record["status"] == "found":
134
+ _write_record(course_dir, record, root=root)
135
+ return record
136
+ last = record
137
+ assert last is not None
138
+ _write_record(course_dir, last, root=root)
139
+ return last
140
+
141
+
142
+ def _surface_index_line(
143
+ course_dir: Path, record: dict[str, object], *, root: Path | None = None
144
+ ) -> None:
145
+ index = course_dir / "INDEX.md"
146
+ try:
147
+ with open(os.fspath(paths.long_path(index)), encoding="utf-8", newline="") as handle:
148
+ lines = handle.read().splitlines()
149
+ except FileNotFoundError:
150
+ return
151
+ cleaned: list[str] = []
152
+ index_position = 0
153
+ while index_position < len(lines):
154
+ if lines[index_position] == _POLICY_START:
155
+ index_position += 1
156
+ while index_position < len(lines) and lines[index_position] != _POLICY_END:
157
+ index_position += 1
158
+ index_position += 1
159
+ continue
160
+ if lines[index_position].strip() == "## AI policy":
161
+ index_position += 1
162
+ while index_position < len(lines) and not _HEADING.match(lines[index_position]):
163
+ index_position += 1
164
+ continue
165
+ cleaned.append(lines[index_position])
166
+ index_position += 1
167
+ status = str(record["status"])
168
+ detail = str(record["source"]) if record["source"] is not None else status
169
+ while cleaned and not cleaned[-1].strip():
170
+ cleaned.pop()
171
+ cleaned.extend(
172
+ [
173
+ "",
174
+ _POLICY_START,
175
+ "## AI policy",
176
+ "",
177
+ f"- AI policy: {status} — {detail}",
178
+ _POLICY_END,
179
+ "",
180
+ ]
181
+ )
182
+ paths.atomic_write_text(index, "\n".join(cleaned), root=root)