skill-tuple-space 0.1.0__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.
- skill_space/__init__.py +3 -0
- skill_space/cli.py +140 -0
- skill_space/display.py +52 -0
- skill_space/embedder.py +17 -0
- skill_space/indexer.py +153 -0
- skill_space/journal.py +40 -0
- skill_space/matcher.py +185 -0
- skill_space/predictor.py +68 -0
- skill_space/store.py +129 -0
- skill_tuple_space-0.1.0.dist-info/METADATA +86 -0
- skill_tuple_space-0.1.0.dist-info/RECORD +14 -0
- skill_tuple_space-0.1.0.dist-info/WHEEL +4 -0
- skill_tuple_space-0.1.0.dist-info/entry_points.txt +2 -0
- skill_tuple_space-0.1.0.dist-info/licenses/LICENSE +21 -0
skill_space/__init__.py
ADDED
skill_space/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""skill-space CLI — Fuzzy Tuple Space for Agent Skills."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
name="skill-space",
|
|
8
|
+
help="Fuzzy Tuple Space for Agent Skills. Index repos, search by fuzzy template, track learning.",
|
|
9
|
+
no_args_is_help=True,
|
|
10
|
+
)
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── Sub-command groups ─────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
index_app = typer.Typer(help="Index skill git repos into the local space.")
|
|
17
|
+
search_app = typer.Typer(help="Search and match skills.")
|
|
18
|
+
learn_app = typer.Typer(help="Learning journal: claim, done, next.")
|
|
19
|
+
space_app = typer.Typer(help="Space health: stats, drift.")
|
|
20
|
+
|
|
21
|
+
app.add_typer(index_app, name="index")
|
|
22
|
+
app.add_typer(search_app, name="search")
|
|
23
|
+
app.add_typer(learn_app, name="learn")
|
|
24
|
+
app.add_typer(space_app, name="space")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── index ──────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@index_app.command("run")
|
|
31
|
+
def index_run(
|
|
32
|
+
repo: str = typer.Option(None, help="Single repo URL to index (overrides config)."),
|
|
33
|
+
force: bool = typer.Option(False, "--force", help="Re-index even if up to date."),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Crawl configured repos, parse SKILL.md files, embed and store."""
|
|
36
|
+
from skill_space.indexer import Indexer
|
|
37
|
+
|
|
38
|
+
indexer = Indexer()
|
|
39
|
+
indexer.run(repo_url=repo, force=force)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── search ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@search_app.command("query")
|
|
46
|
+
def search_query(
|
|
47
|
+
query: str = typer.Argument(
|
|
48
|
+
..., help="Free-text query, e.g. 'create something visual with 3d'"
|
|
49
|
+
),
|
|
50
|
+
skill_class: str = typer.Option(
|
|
51
|
+
None, "--class", help="Filter: Role | Topic | Process | OneStepProcess"
|
|
52
|
+
),
|
|
53
|
+
lang: str = typer.Option("en", "--lang", help="Language filter."),
|
|
54
|
+
top: int = typer.Option(5, "--top", help="Number of results."),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Semantic + fuzzy search across all indexed skills."""
|
|
57
|
+
from skill_space.matcher import Matcher
|
|
58
|
+
|
|
59
|
+
matcher = Matcher()
|
|
60
|
+
results = matcher.search(query=query, skill_class=skill_class, lang=lang, top=top)
|
|
61
|
+
from skill_space.display import display_results
|
|
62
|
+
|
|
63
|
+
display_results(results)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@search_app.command("read")
|
|
67
|
+
def search_read(
|
|
68
|
+
template: str = typer.Argument(
|
|
69
|
+
...,
|
|
70
|
+
help="Linda-style template, e.g. 'skill_class=Process, crud_verb=create, topic=*'",
|
|
71
|
+
),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Template-based tuple matching (JavaSpaces / Linda style)."""
|
|
74
|
+
from skill_space.matcher import Matcher
|
|
75
|
+
|
|
76
|
+
matcher = Matcher()
|
|
77
|
+
results = matcher.read_template(template)
|
|
78
|
+
from skill_space.display import display_results
|
|
79
|
+
|
|
80
|
+
display_results(results)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── learn ──────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@learn_app.command("claim")
|
|
87
|
+
def learn_claim(skill_name: str = typer.Argument(...)) -> None:
|
|
88
|
+
"""Mark a skill as 'currently learning'."""
|
|
89
|
+
from skill_space.journal import Journal
|
|
90
|
+
|
|
91
|
+
Journal().claim(skill_name)
|
|
92
|
+
console.print(f"[green]Claimed:[/green] {skill_name}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@learn_app.command("done")
|
|
96
|
+
def learn_done(
|
|
97
|
+
skill_name: str = typer.Argument(...),
|
|
98
|
+
level: int = typer.Option(..., "--level", min=1, max=5, help="Mastery level reached (1–5)."),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Record completion of a skill at a given mastery level."""
|
|
101
|
+
from skill_space.journal import Journal
|
|
102
|
+
|
|
103
|
+
Journal().done(skill_name, level=level)
|
|
104
|
+
console.print(f"[green]Done:[/green] {skill_name} at level {level}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@learn_app.command("next")
|
|
108
|
+
def learn_next(
|
|
109
|
+
topic: str = typer.Option(None, "--topic", help="Filter suggestions by topic."),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Predict the best next skill to learn based on your journal."""
|
|
112
|
+
from skill_space.predictor import Predictor
|
|
113
|
+
|
|
114
|
+
suggestion = Predictor().next_skill(topic=topic)
|
|
115
|
+
from skill_space.display import display_suggestion
|
|
116
|
+
|
|
117
|
+
display_suggestion(suggestion)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── space ──────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@space_app.command("stats")
|
|
124
|
+
def space_stats() -> None:
|
|
125
|
+
"""Show space statistics: repos indexed, skill count, last sync."""
|
|
126
|
+
from skill_space.store import Store
|
|
127
|
+
|
|
128
|
+
Store().print_stats()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@space_app.command("drift")
|
|
132
|
+
def space_drift() -> None:
|
|
133
|
+
"""Detect stale skills: pinned commits behind HEAD, old last-touched dates."""
|
|
134
|
+
from skill_space.indexer import Indexer
|
|
135
|
+
|
|
136
|
+
Indexer().drift_report()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
app()
|
skill_space/display.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Display — rich terminal output for search results and suggestions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from skill_space.matcher import SkillMatch
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def display_results(results: list[SkillMatch]) -> None:
|
|
16
|
+
if not results:
|
|
17
|
+
console.print("[yellow]No matches found.[/yellow]")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
t = Table(title="Skill Space Results")
|
|
21
|
+
t.add_column("Score", style="cyan", width=6)
|
|
22
|
+
t.add_column("Skill Name", style="bold")
|
|
23
|
+
t.add_column("Class", width=14)
|
|
24
|
+
t.add_column("Topic", width=16)
|
|
25
|
+
t.add_column("Why", style="dim")
|
|
26
|
+
|
|
27
|
+
for r in results:
|
|
28
|
+
s = r.skill
|
|
29
|
+
t.add_row(
|
|
30
|
+
f"{r.final_score:.2f}",
|
|
31
|
+
s["name"],
|
|
32
|
+
s.get("skill_class") or "?",
|
|
33
|
+
s.get("topic") or "?",
|
|
34
|
+
", ".join(r.reasons),
|
|
35
|
+
)
|
|
36
|
+
console.print(t)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def display_suggestion(suggestion: Optional[SkillMatch]) -> None:
|
|
40
|
+
if suggestion is None:
|
|
41
|
+
console.print(
|
|
42
|
+
"[yellow]No suggestion available — index more skills or complete more learning events.[/yellow]"
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
s = suggestion.skill
|
|
47
|
+
console.print(f"\n[bold green]Suggested next skill:[/bold green] {s['name']}")
|
|
48
|
+
console.print(f" Class: {s.get('skill_class', '?')}")
|
|
49
|
+
console.print(f" Topic: {s.get('topic', '?')}")
|
|
50
|
+
console.print(f" Score: {suggestion.final_score:.2f}")
|
|
51
|
+
console.print(f" Reasons: {', '.join(suggestion.reasons)}")
|
|
52
|
+
console.print(f" Repo: {s.get('repo_url', '?')}\n")
|
skill_space/embedder.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Embedder — thin wrapper around sentence-transformers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache(maxsize=1)
|
|
9
|
+
def _model():
|
|
10
|
+
from sentence_transformers import SentenceTransformer
|
|
11
|
+
|
|
12
|
+
return SentenceTransformer("all-MiniLM-L6-v2") # 80MB, 384-dim, offline-capable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Embedder:
|
|
16
|
+
def encode(self, text: str) -> list[float]:
|
|
17
|
+
return _model().encode(text, normalize_embeddings=True).tolist()
|
skill_space/indexer.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Indexer — crawl skill git repos, parse SKILL.md, embed, store."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from skill_space.store import Store
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
CONFIG_PATH = Path.home() / ".skill-space" / "config.toml"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_config() -> dict:
|
|
21
|
+
if not CONFIG_PATH.exists():
|
|
22
|
+
return {"repos": []}
|
|
23
|
+
import tomllib # py 3.11+
|
|
24
|
+
|
|
25
|
+
return tomllib.loads(CONFIG_PATH.read_text())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_skill_md(path: Path) -> Optional[dict]:
|
|
29
|
+
"""Parse frontmatter from a SKILL.md file."""
|
|
30
|
+
text = path.read_text(errors="replace")
|
|
31
|
+
match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
|
|
32
|
+
if not match:
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
fm = yaml.safe_load(match.group(1))
|
|
36
|
+
except yaml.YAMLError:
|
|
37
|
+
return None
|
|
38
|
+
# Extract body description (first non-empty line after frontmatter)
|
|
39
|
+
body = text[match.end() :].strip()
|
|
40
|
+
first_para = body.split("\n\n")[0].strip().lstrip("#").strip()
|
|
41
|
+
return {
|
|
42
|
+
"name": fm.get("name", path.parent.name),
|
|
43
|
+
"skill_class": fm.get("metadata", {}).get("skill-class")
|
|
44
|
+
if isinstance(fm.get("metadata"), dict)
|
|
45
|
+
else None,
|
|
46
|
+
"crud_verb": fm.get("metadata", {}).get("crud-verb")
|
|
47
|
+
if isinstance(fm.get("metadata"), dict)
|
|
48
|
+
else None,
|
|
49
|
+
"topic": fm.get("metadata", {}).get("topic")
|
|
50
|
+
if isinstance(fm.get("metadata"), dict)
|
|
51
|
+
else None,
|
|
52
|
+
"requires_role": fm.get("metadata", {}).get("requires-role")
|
|
53
|
+
if isinstance(fm.get("metadata"), dict)
|
|
54
|
+
else None,
|
|
55
|
+
"language": "en"
|
|
56
|
+
if path.parent.name.endswith("-en")
|
|
57
|
+
else fm.get("metadata", {}).get("lang", "en")
|
|
58
|
+
if isinstance(fm.get("metadata"), dict)
|
|
59
|
+
else "en",
|
|
60
|
+
"description": first_para,
|
|
61
|
+
"fuzzy_tags": json.dumps(_extract_tags(fm, body)),
|
|
62
|
+
"last_indexed": datetime.now(timezone.utc).isoformat(),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_tags(fm: dict, body: str) -> list[str]:
|
|
67
|
+
"""Lightweight tag extraction from frontmatter + body keywords."""
|
|
68
|
+
tags: set[str] = set()
|
|
69
|
+
desc = str(fm.get("description", ""))
|
|
70
|
+
for word in re.findall(r"\b[a-z][a-z0-9\-]{3,}\b", (desc + " " + body).lower()):
|
|
71
|
+
if word not in {"this", "that", "with", "from", "when", "skill", "user", "agent"}:
|
|
72
|
+
tags.add(word)
|
|
73
|
+
return sorted(tags)[:30] # cap at 30
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Indexer:
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self.store = Store()
|
|
79
|
+
|
|
80
|
+
def run(self, repo_url: Optional[str] = None, force: bool = False) -> None:
|
|
81
|
+
config = _load_config()
|
|
82
|
+
repos = [{"url": repo_url, "trust": "high"}] if repo_url else config.get("repos", [])
|
|
83
|
+
|
|
84
|
+
if not repos:
|
|
85
|
+
console.print(
|
|
86
|
+
"[yellow]No repos configured. Add repos to ~/.skill-space/config.toml[/yellow]"
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
for repo_conf in repos:
|
|
91
|
+
self._index_repo(repo_conf, force=force)
|
|
92
|
+
|
|
93
|
+
def _index_repo(self, repo_conf: dict, force: bool) -> None:
|
|
94
|
+
import tempfile
|
|
95
|
+
import git # gitpython
|
|
96
|
+
|
|
97
|
+
url = repo_conf["url"]
|
|
98
|
+
trust = repo_conf.get("trust", "high")
|
|
99
|
+
console.print(f"[cyan]Indexing[/cyan] {url} (trust={trust})")
|
|
100
|
+
|
|
101
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
102
|
+
try:
|
|
103
|
+
grepo = git.Repo.clone_from(url, tmpdir, depth=1)
|
|
104
|
+
commit = grepo.head.commit.hexsha[:8]
|
|
105
|
+
except Exception as e:
|
|
106
|
+
console.print(f"[red]Clone failed:[/red] {e}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
skills_dir = Path(tmpdir) / "skills"
|
|
110
|
+
if not skills_dir.exists():
|
|
111
|
+
skills_dir = Path(tmpdir)
|
|
112
|
+
|
|
113
|
+
count = 0
|
|
114
|
+
for skill_md in skills_dir.rglob("SKILL.md"):
|
|
115
|
+
parsed = _parse_skill_md(skill_md)
|
|
116
|
+
if not parsed:
|
|
117
|
+
continue
|
|
118
|
+
parsed["repo_url"] = url
|
|
119
|
+
parsed["repo_trust"] = trust
|
|
120
|
+
parsed["pinned_commit"] = commit
|
|
121
|
+
skill_id = self.store.upsert_skill(parsed)
|
|
122
|
+
self._embed_and_store(skill_id, parsed)
|
|
123
|
+
count += 1
|
|
124
|
+
|
|
125
|
+
console.print(f" [green]✓[/green] {count} skills indexed")
|
|
126
|
+
|
|
127
|
+
def _embed_and_store(self, skill_id: int, parsed: dict) -> None:
|
|
128
|
+
try:
|
|
129
|
+
from skill_space.embedder import Embedder
|
|
130
|
+
|
|
131
|
+
text = f"{parsed['name']} {parsed.get('topic', '')} {parsed.get('description', '')}"
|
|
132
|
+
vec = Embedder().encode(text)
|
|
133
|
+
self.store.store_embedding(skill_id, vec)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass # embeddings optional — fuzzy-only fallback still works
|
|
136
|
+
|
|
137
|
+
def drift_report(self) -> None:
|
|
138
|
+
from rich.table import Table
|
|
139
|
+
|
|
140
|
+
skills = self.store.all_skills()
|
|
141
|
+
t = Table(title="Drift Report — Potentially Stale Skills")
|
|
142
|
+
t.add_column("Skill")
|
|
143
|
+
t.add_column("Pinned Commit")
|
|
144
|
+
t.add_column("Last Indexed")
|
|
145
|
+
t.add_column("Repo Trust")
|
|
146
|
+
for s in skills:
|
|
147
|
+
t.add_row(
|
|
148
|
+
s["name"],
|
|
149
|
+
s.get("pinned_commit", "?"),
|
|
150
|
+
s.get("last_indexed", "?")[:10],
|
|
151
|
+
s.get("repo_trust", "?"),
|
|
152
|
+
)
|
|
153
|
+
Console().print(t)
|
skill_space/journal.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Journal — learning events (claim, done) stored in SQLite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from skill_space.store import Store
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Journal:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.store = Store()
|
|
13
|
+
|
|
14
|
+
def claim(self, skill_name: str) -> None:
|
|
15
|
+
self.store.conn.execute(
|
|
16
|
+
"INSERT INTO learning_journal(skill_name, event, timestamp) VALUES (?, 'claimed', ?)",
|
|
17
|
+
(skill_name, datetime.now(timezone.utc).isoformat()),
|
|
18
|
+
)
|
|
19
|
+
self.store.conn.commit()
|
|
20
|
+
|
|
21
|
+
def done(self, skill_name: str, level: int) -> None:
|
|
22
|
+
self.store.conn.execute(
|
|
23
|
+
"INSERT INTO learning_journal(skill_name, event, level, timestamp) VALUES (?, 'done', ?, ?)",
|
|
24
|
+
(skill_name, level, datetime.now(timezone.utc).isoformat()),
|
|
25
|
+
)
|
|
26
|
+
self.store.conn.commit()
|
|
27
|
+
|
|
28
|
+
def completed_names(self) -> set[str]:
|
|
29
|
+
cur = self.store.conn.execute(
|
|
30
|
+
"SELECT DISTINCT skill_name FROM learning_journal WHERE event='done'"
|
|
31
|
+
)
|
|
32
|
+
return {row[0] for row in cur.fetchall()}
|
|
33
|
+
|
|
34
|
+
def max_level(self, skill_name: str) -> int:
|
|
35
|
+
cur = self.store.conn.execute(
|
|
36
|
+
"SELECT MAX(level) FROM learning_journal WHERE skill_name=? AND event='done'",
|
|
37
|
+
(skill_name,),
|
|
38
|
+
)
|
|
39
|
+
val = cur.fetchone()[0]
|
|
40
|
+
return val or 0
|
skill_space/matcher.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Matcher — fuzzy metadata scoring + semantic RAG, combined.
|
|
2
|
+
|
|
3
|
+
Two distinct layers:
|
|
4
|
+
1. FuzzyMatcher — operates on structured metadata fields (skill_class, crud_verb,
|
|
5
|
+
topic, requires_role). Uses membership functions, not cosine.
|
|
6
|
+
2. SemanticMatcher — cosine similarity on description embeddings (RAG layer).
|
|
7
|
+
|
|
8
|
+
Final score = w_fuzzy * fuzzy_score + w_semantic * semantic_score
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from skill_space.store import Store
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Fuzzy membership functions ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
# Skill class hierarchy — partial membership across adjacent classes
|
|
24
|
+
_CLASS_MEMBERSHIP: dict[str, dict[str, float]] = {
|
|
25
|
+
"OneStepProcess": {"OneStepProcess": 1.0, "Process": 0.7, "Topic": 0.1, "Role": 0.0},
|
|
26
|
+
"Process": {"Process": 1.0, "OneStepProcess": 0.6, "Topic": 0.2, "Role": 0.0},
|
|
27
|
+
"Topic": {"Topic": 1.0, "Process": 0.3, "Role": 0.1, "OneStepProcess": 0.2},
|
|
28
|
+
"Role": {"Role": 1.0, "Topic": 0.1, "Process": 0.0, "OneStepProcess": 0.0},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fuzzy_class_match(query_class: Optional[str], skill_class: Optional[str]) -> float:
|
|
33
|
+
"""Degree to which skill_class satisfies query_class."""
|
|
34
|
+
if query_class is None or skill_class is None:
|
|
35
|
+
return 0.5 # unknown → neutral, not zero
|
|
36
|
+
return _CLASS_MEMBERSHIP.get(skill_class, {}).get(query_class, 0.0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fuzzy_token_overlap(query_tokens: list[str], skill_tags: list[str]) -> float:
|
|
40
|
+
"""Jaccard-ish overlap between query tokens and skill fuzzy_tags."""
|
|
41
|
+
if not query_tokens or not skill_tags:
|
|
42
|
+
return 0.0
|
|
43
|
+
hits = sum(1 for t in query_tokens if any(t in tag or tag in t for tag in skill_tags))
|
|
44
|
+
return hits / len(query_tokens)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fuzzy_trust(trust: str) -> float:
|
|
48
|
+
return {"high": 1.0, "medium": 0.7, "low": 0.4}.get(trust, 0.5)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Template parser (Linda-style) ──────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_template(template: str) -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Parse 'skill_class=Process, crud_verb=create, topic=*' into a dict.
|
|
57
|
+
'*' means wildcard (no filter). '~Process' means fuzzy match.
|
|
58
|
+
"""
|
|
59
|
+
result: dict[str, str] = {}
|
|
60
|
+
for part in re.split(r",\s*", template):
|
|
61
|
+
if "=" in part:
|
|
62
|
+
k, v = part.split("=", 1)
|
|
63
|
+
result[k.strip()] = v.strip()
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Result dataclass ───────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SkillMatch:
|
|
72
|
+
skill: dict
|
|
73
|
+
fuzzy_score: float = 0.0
|
|
74
|
+
semantic_score: float = 0.0
|
|
75
|
+
final_score: float = 0.0
|
|
76
|
+
reasons: list[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Matcher ────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
W_FUZZY = 0.45
|
|
82
|
+
W_SEMANTIC = 0.55
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Matcher:
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self.store = Store()
|
|
88
|
+
|
|
89
|
+
def search(
|
|
90
|
+
self,
|
|
91
|
+
query: str,
|
|
92
|
+
skill_class: Optional[str] = None,
|
|
93
|
+
lang: str = "en",
|
|
94
|
+
top: int = 5,
|
|
95
|
+
) -> list[SkillMatch]:
|
|
96
|
+
skills = self.store.all_skills()
|
|
97
|
+
query_tokens = re.findall(r"\b[a-z]{3,}\b", query.lower())
|
|
98
|
+
|
|
99
|
+
# Semantic embedding for the query
|
|
100
|
+
sem_scores: dict[str, float] = {}
|
|
101
|
+
try:
|
|
102
|
+
from skill_space.embedder import Embedder
|
|
103
|
+
|
|
104
|
+
q_vec = Embedder().encode(query)
|
|
105
|
+
sem_scores = self._cosine_all(q_vec, skills)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
results: list[SkillMatch] = []
|
|
110
|
+
for s in skills:
|
|
111
|
+
if lang and s.get("language") != lang:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
tags = json.loads(s.get("fuzzy_tags") or "[]")
|
|
115
|
+
|
|
116
|
+
# Fuzzy layer
|
|
117
|
+
class_score = fuzzy_class_match(skill_class, s.get("skill_class"))
|
|
118
|
+
tag_score = fuzzy_token_overlap(query_tokens, tags)
|
|
119
|
+
trust_score = fuzzy_trust(s.get("repo_trust", "high"))
|
|
120
|
+
fuzzy = class_score * 0.4 + tag_score * 0.5 + trust_score * 0.1
|
|
121
|
+
|
|
122
|
+
# Semantic layer
|
|
123
|
+
semantic = sem_scores.get(s["name"], 0.0)
|
|
124
|
+
|
|
125
|
+
final = W_FUZZY * fuzzy + W_SEMANTIC * semantic
|
|
126
|
+
|
|
127
|
+
reasons: list[str] = []
|
|
128
|
+
if class_score > 0.5:
|
|
129
|
+
reasons.append(f"class~{class_score:.2f}")
|
|
130
|
+
if tag_score > 0.3:
|
|
131
|
+
reasons.append(f"tags~{tag_score:.2f}")
|
|
132
|
+
if semantic > 0.4:
|
|
133
|
+
reasons.append(f"semantic~{semantic:.2f}")
|
|
134
|
+
|
|
135
|
+
results.append(SkillMatch(s, fuzzy, semantic, final, reasons))
|
|
136
|
+
|
|
137
|
+
results.sort(key=lambda r: r.final_score, reverse=True)
|
|
138
|
+
return results[:top]
|
|
139
|
+
|
|
140
|
+
def read_template(self, template: str) -> list[SkillMatch]:
|
|
141
|
+
"""Linda-style template match with fuzzy class membership."""
|
|
142
|
+
tmpl = parse_template(template)
|
|
143
|
+
skills = self.store.all_skills()
|
|
144
|
+
results: list[SkillMatch] = []
|
|
145
|
+
|
|
146
|
+
for s in skills:
|
|
147
|
+
score = 1.0
|
|
148
|
+
reasons: list[str] = []
|
|
149
|
+
for field_name, value in tmpl.items():
|
|
150
|
+
if value == "*":
|
|
151
|
+
continue
|
|
152
|
+
v = value.lstrip("~")
|
|
153
|
+
skill_val = s.get(field_name, "")
|
|
154
|
+
|
|
155
|
+
if field_name == "skill_class":
|
|
156
|
+
m = fuzzy_class_match(v, skill_val)
|
|
157
|
+
else:
|
|
158
|
+
m = (
|
|
159
|
+
1.0
|
|
160
|
+
if skill_val == v
|
|
161
|
+
else (0.5 if v.lower() in str(skill_val).lower() else 0.0)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
score *= m
|
|
165
|
+
reasons.append(f"{field_name}={m:.2f}")
|
|
166
|
+
|
|
167
|
+
if score > 0.0:
|
|
168
|
+
results.append(SkillMatch(s, fuzzy_score=score, final_score=score, reasons=reasons))
|
|
169
|
+
|
|
170
|
+
results.sort(key=lambda r: r.final_score, reverse=True)
|
|
171
|
+
return results[:10]
|
|
172
|
+
|
|
173
|
+
def _cosine_all(self, q_vec: list[float], skills: list[dict]) -> dict[str, float]:
|
|
174
|
+
import numpy as np
|
|
175
|
+
from skill_space.embedder import Embedder
|
|
176
|
+
|
|
177
|
+
emb = Embedder()
|
|
178
|
+
scores: dict[str, float] = {}
|
|
179
|
+
q = np.array(q_vec)
|
|
180
|
+
for s in skills:
|
|
181
|
+
text = f"{s['name']} {s.get('topic', '')} {s.get('description', '')}"
|
|
182
|
+
v = np.array(emb.encode(text))
|
|
183
|
+
cos = float(np.dot(q, v) / (np.linalg.norm(q) * np.linalg.norm(v) + 1e-9))
|
|
184
|
+
scores[s["name"]] = cos
|
|
185
|
+
return scores
|
skill_space/predictor.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Predictor — combines fuzzy matching + learning journal to suggest next skill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from skill_space.journal import Journal
|
|
8
|
+
from skill_space.matcher import Matcher, SkillMatch, fuzzy_trust
|
|
9
|
+
from skill_space.store import Store
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Predictor:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.journal = Journal()
|
|
15
|
+
self.store = Store()
|
|
16
|
+
self.matcher = Matcher()
|
|
17
|
+
|
|
18
|
+
def next_skill(self, topic: Optional[str] = None) -> Optional[SkillMatch]:
|
|
19
|
+
completed = self.journal.completed_names()
|
|
20
|
+
skills = self.store.all_skills()
|
|
21
|
+
|
|
22
|
+
candidates: list[SkillMatch] = []
|
|
23
|
+
for s in skills:
|
|
24
|
+
if s["name"] in completed:
|
|
25
|
+
continue
|
|
26
|
+
if topic and topic.lower() not in (s.get("topic") or "").lower():
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
readiness = self._readiness(s, completed)
|
|
30
|
+
trust = fuzzy_trust(s.get("repo_trust", "high"))
|
|
31
|
+
final = readiness * 0.7 + trust * 0.3
|
|
32
|
+
|
|
33
|
+
candidates.append(
|
|
34
|
+
SkillMatch(
|
|
35
|
+
skill=s,
|
|
36
|
+
fuzzy_score=readiness,
|
|
37
|
+
final_score=final,
|
|
38
|
+
reasons=[f"readiness~{readiness:.2f}", f"trust~{trust:.2f}"],
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
candidates.sort(key=lambda c: c.final_score, reverse=True)
|
|
43
|
+
return candidates[0] if candidates else None
|
|
44
|
+
|
|
45
|
+
def _readiness(self, skill: dict, completed: set[str]) -> float:
|
|
46
|
+
"""
|
|
47
|
+
Heuristic readiness score:
|
|
48
|
+
- If skill has requires_role and that role-skill is not yet done → penalise
|
|
49
|
+
- If skill_class is Process and user has done at least 2 Topic-class skills → boost
|
|
50
|
+
- Otherwise neutral
|
|
51
|
+
"""
|
|
52
|
+
score = 0.5
|
|
53
|
+
|
|
54
|
+
req_role = skill.get("requires_role")
|
|
55
|
+
if req_role:
|
|
56
|
+
role_done = any(req_role in name for name in completed)
|
|
57
|
+
score += 0.3 if role_done else -0.2
|
|
58
|
+
|
|
59
|
+
skill_class = skill.get("skill_class", "")
|
|
60
|
+
if skill_class == "Process":
|
|
61
|
+
topic_done_count = sum(1 for name in completed if self._class_of(name) == "Topic")
|
|
62
|
+
score += min(topic_done_count * 0.05, 0.2)
|
|
63
|
+
|
|
64
|
+
return max(0.0, min(1.0, score))
|
|
65
|
+
|
|
66
|
+
def _class_of(self, skill_name: str) -> Optional[str]:
|
|
67
|
+
all_skills = {s["name"]: s for s in self.store.all_skills()}
|
|
68
|
+
return all_skills.get(skill_name, {}).get("skill_class")
|
skill_space/store.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Store — SQLite + sqlite-vec persistence layer for the Skill Space."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
DB_PATH = Path.home() / ".skill-space" / "space.db"
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CREATE_SKILLS = """
|
|
16
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
name TEXT NOT NULL UNIQUE,
|
|
19
|
+
skill_class TEXT, -- Role | Topic | Process | OneStepProcess
|
|
20
|
+
crud_verb TEXT,
|
|
21
|
+
topic TEXT,
|
|
22
|
+
requires_role TEXT,
|
|
23
|
+
language TEXT DEFAULT 'en',
|
|
24
|
+
repo_url TEXT,
|
|
25
|
+
repo_trust TEXT DEFAULT 'high', -- high | medium | low
|
|
26
|
+
pinned_commit TEXT,
|
|
27
|
+
fuzzy_tags TEXT, -- JSON array of strings
|
|
28
|
+
description TEXT,
|
|
29
|
+
last_indexed TEXT -- ISO-8601
|
|
30
|
+
);
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
CREATE_REPOS = """
|
|
34
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
url TEXT NOT NULL UNIQUE,
|
|
37
|
+
trust TEXT DEFAULT 'high',
|
|
38
|
+
last_synced TEXT
|
|
39
|
+
);
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
CREATE_JOURNAL = """
|
|
43
|
+
CREATE TABLE IF NOT EXISTS learning_journal (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
skill_name TEXT NOT NULL,
|
|
46
|
+
event TEXT NOT NULL, -- 'claimed' | 'done'
|
|
47
|
+
level INTEGER,
|
|
48
|
+
timestamp TEXT NOT NULL -- ISO-8601
|
|
49
|
+
);
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# sqlite-vec virtual table for embeddings (created after vec extension loaded)
|
|
53
|
+
CREATE_SKILL_VECS = """
|
|
54
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skill_vecs USING vec0(
|
|
55
|
+
skill_id INTEGER PRIMARY KEY,
|
|
56
|
+
embedding FLOAT[384]
|
|
57
|
+
);
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Store:
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
self.conn = sqlite3.connect(DB_PATH)
|
|
65
|
+
self._init_schema()
|
|
66
|
+
|
|
67
|
+
def _init_schema(self) -> None:
|
|
68
|
+
try:
|
|
69
|
+
import sqlite_vec
|
|
70
|
+
|
|
71
|
+
self.conn.load_extension(sqlite_vec.loadable_path())
|
|
72
|
+
self.conn.execute(CREATE_SKILL_VECS)
|
|
73
|
+
except Exception:
|
|
74
|
+
console.print("[yellow]sqlite-vec not available — vector search disabled.[/yellow]")
|
|
75
|
+
self.conn.executescript(CREATE_SKILLS + CREATE_REPOS + CREATE_JOURNAL)
|
|
76
|
+
self.conn.commit()
|
|
77
|
+
|
|
78
|
+
def upsert_skill(self, skill: dict) -> int:
|
|
79
|
+
cur = self.conn.execute(
|
|
80
|
+
"""
|
|
81
|
+
INSERT INTO skills
|
|
82
|
+
(name, skill_class, crud_verb, topic, requires_role, language,
|
|
83
|
+
repo_url, repo_trust, pinned_commit, fuzzy_tags, description, last_indexed)
|
|
84
|
+
VALUES
|
|
85
|
+
(:name, :skill_class, :crud_verb, :topic, :requires_role, :language,
|
|
86
|
+
:repo_url, :repo_trust, :pinned_commit, :fuzzy_tags, :description, :last_indexed)
|
|
87
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
88
|
+
skill_class=excluded.skill_class,
|
|
89
|
+
crud_verb=excluded.crud_verb,
|
|
90
|
+
topic=excluded.topic,
|
|
91
|
+
requires_role=excluded.requires_role,
|
|
92
|
+
fuzzy_tags=excluded.fuzzy_tags,
|
|
93
|
+
description=excluded.description,
|
|
94
|
+
last_indexed=excluded.last_indexed
|
|
95
|
+
RETURNING id
|
|
96
|
+
""",
|
|
97
|
+
skill,
|
|
98
|
+
)
|
|
99
|
+
row = cur.fetchone()[0]
|
|
100
|
+
self.conn.commit()
|
|
101
|
+
return row
|
|
102
|
+
|
|
103
|
+
def store_embedding(self, skill_id: int, vector: list[float]) -> None:
|
|
104
|
+
import json
|
|
105
|
+
|
|
106
|
+
self.conn.execute(
|
|
107
|
+
"INSERT OR REPLACE INTO skill_vecs(skill_id, embedding) VALUES (?, ?)",
|
|
108
|
+
(skill_id, json.dumps(vector)),
|
|
109
|
+
)
|
|
110
|
+
self.conn.commit()
|
|
111
|
+
|
|
112
|
+
def all_skills(self) -> list[dict]:
|
|
113
|
+
cur = self.conn.execute("SELECT * FROM skills")
|
|
114
|
+
cols = [d[0] for d in cur.description]
|
|
115
|
+
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
116
|
+
|
|
117
|
+
def print_stats(self) -> None:
|
|
118
|
+
skill_count = self.conn.execute("SELECT COUNT(*) FROM skills").fetchone()[0]
|
|
119
|
+
repo_count = self.conn.execute("SELECT COUNT(*) FROM repos").fetchone()[0]
|
|
120
|
+
journal_count = self.conn.execute("SELECT COUNT(*) FROM learning_journal").fetchone()[0]
|
|
121
|
+
|
|
122
|
+
t = Table(title="Skill Space Stats")
|
|
123
|
+
t.add_column("Metric")
|
|
124
|
+
t.add_column("Value", style="cyan")
|
|
125
|
+
t.add_row("Skills indexed", str(skill_count))
|
|
126
|
+
t.add_row("Repos configured", str(repo_count))
|
|
127
|
+
t.add_row("Learning events", str(journal_count))
|
|
128
|
+
t.add_row("DB path", str(DB_PATH))
|
|
129
|
+
console.print(t)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skill-tuple-space
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fuzzy Tuple Space for Agent Skills — index, search, and predict across skill git repos
|
|
5
|
+
Author-email: roebi <roebi@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: gitpython>=3.1
|
|
10
|
+
Requires-Dist: pyyaml>=6
|
|
11
|
+
Requires-Dist: rich>=13
|
|
12
|
+
Requires-Dist: sentence-transformers>=3
|
|
13
|
+
Requires-Dist: sqlite-vec>=0.1
|
|
14
|
+
Requires-Dist: typer>=0.12
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
19
|
+
Requires-Dist: types-pyyaml>=6; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# skill-tuple-space
|
|
23
|
+
|
|
24
|
+
**Fuzzy Tuple Space for Agent Skills.**
|
|
25
|
+
|
|
26
|
+
Inspired by David Gelernter's Linda coordination language and JavaSpaces,
|
|
27
|
+
`skill-tuple-space` indexes your agent skill git repos into a local SQLite store
|
|
28
|
+
and lets you query them using two complementary mechanisms:
|
|
29
|
+
|
|
30
|
+
- **Fuzzy layer** — membership-function scoring on structured metadata
|
|
31
|
+
(`skill_class`, `crud_verb`, `topic`, `requires_role`).
|
|
32
|
+
- **Semantic layer** — RAG / cosine similarity on SKILL.md descriptions
|
|
33
|
+
via `sentence-transformers`.
|
|
34
|
+
|
|
35
|
+
These are **two distinct things**: the semantic layer finds what is _similar_,
|
|
36
|
+
the fuzzy layer reasons about how well something _fits your current taxonomy and context_.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv pip install -e ".[dev]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Copy and edit config
|
|
48
|
+
mkdir -p ~/.skill-space
|
|
49
|
+
cp config.example.toml ~/.skill-space/config.toml
|
|
50
|
+
|
|
51
|
+
# 2. Index your repos
|
|
52
|
+
skill-space index run
|
|
53
|
+
|
|
54
|
+
# 3. Semantic + fuzzy search
|
|
55
|
+
skill-space search query "create something visual with 3d"
|
|
56
|
+
|
|
57
|
+
# 4. Linda-style template matching
|
|
58
|
+
skill-space search read "skill_class=Process, crud_verb=create, topic=*"
|
|
59
|
+
|
|
60
|
+
# 5. Learning journal
|
|
61
|
+
skill-space learn claim create-openscad-from-construction-image-en
|
|
62
|
+
skill-space learn done create-openscad-from-construction-image-en --level 4
|
|
63
|
+
skill-space learn next --topic agent-skills
|
|
64
|
+
|
|
65
|
+
# 6. Space health
|
|
66
|
+
skill-space space stats
|
|
67
|
+
skill-space space drift
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Architecture
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
src/skill_space/
|
|
74
|
+
├── cli.py # typer CLI entry point
|
|
75
|
+
├── indexer.py # git clone + SKILL.md parse + embed
|
|
76
|
+
├── embedder.py # sentence-transformers (all-MiniLM-L6-v2, 384-dim)
|
|
77
|
+
├── store.py # SQLite + sqlite-vec persistence
|
|
78
|
+
├── matcher.py # fuzzy membership functions + cosine combiner
|
|
79
|
+
├── journal.py # learning events CRUD
|
|
80
|
+
├── predictor.py # readiness scoring + next-skill suggestion
|
|
81
|
+
└── display.py # rich terminal output
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
skill_space/__init__.py,sha256=CjtWVtpQi40Mx3mz23pJUzg4VK7t98TNL9VUlMWKz30,81
|
|
2
|
+
skill_space/cli.py,sha256=ZV-fKqK3hVC7B4x_jF4igLkIs-mDDbaE0XyeSAiKn5Q,5021
|
|
3
|
+
skill_space/display.py,sha256=Rr9WHhy7uKVPGiyHx29cElYIQ4CnYdskNZ4xLamgMD0,1601
|
|
4
|
+
skill_space/embedder.py,sha256=y10-8i1y2AthMBY0rzTsWMfGU9MnNbVjRX6iBdTCZik,452
|
|
5
|
+
skill_space/indexer.py,sha256=shwTA379ncePXVhLZhkASlxqpJ1Vxy1jGKxebEs2D4w,5271
|
|
6
|
+
skill_space/journal.py,sha256=ZM1H81W4wLsHSrmSOml7UwnJ974nALJlFJQnENXpUzQ,1357
|
|
7
|
+
skill_space/matcher.py,sha256=QpE0Na7dckXxCroZ3JA4xPFeCU1ZGCGAPFW_c3oe404,6753
|
|
8
|
+
skill_space/predictor.py,sha256=kj3w15AKj4byB1PdpQXJmbhUJhrVXO9QhZ6vQAeF5GY,2434
|
|
9
|
+
skill_space/store.py,sha256=GvDwgvI0PlTrpua4r_54F3G29EXetdsk7ZdVmYGLQNo,4374
|
|
10
|
+
skill_tuple_space-0.1.0.dist-info/METADATA,sha256=HsuZYNNLMf6jQv7SptB5O-3hs2LiMzU1aH2r1v6hXFY,2601
|
|
11
|
+
skill_tuple_space-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
skill_tuple_space-0.1.0.dist-info/entry_points.txt,sha256=r0jskIV_dqEqLSxxAg7-MGak-VXaPSYApEuDyAdZZ0I,52
|
|
13
|
+
skill_tuple_space-0.1.0.dist-info/licenses/LICENSE,sha256=4cVpnwal-aV40hsbwx8zPPXWeKgqHWL249D4jwdAy1U,1070
|
|
14
|
+
skill_tuple_space-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Halter
|
|
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.
|