ctxeng 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.
- ctxeng/__init__.py +11 -0
- ctxeng/builder.py +115 -0
- ctxeng/cli.py +152 -0
- ctxeng/core.py +156 -0
- ctxeng/integrations/__init__.py +145 -0
- ctxeng/models.py +151 -0
- ctxeng/optimizer.py +131 -0
- ctxeng/scorer.py +150 -0
- ctxeng/sources/__init__.py +171 -0
- ctxeng-0.1.0.dist-info/METADATA +412 -0
- ctxeng-0.1.0.dist-info/RECORD +14 -0
- ctxeng-0.1.0.dist-info/WHEEL +4 -0
- ctxeng-0.1.0.dist-info/entry_points.txt +2 -0
- ctxeng-0.1.0.dist-info/licenses/LICENSE +21 -0
ctxeng/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ctxeng — Python Context Engineering Library
|
|
3
|
+
Automatically build, compress, and inject perfect LLM context from your codebase.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ctxeng.builder import ContextBuilder
|
|
7
|
+
from ctxeng.core import ContextEngine
|
|
8
|
+
from ctxeng.models import Context, ContextFile, TokenBudget
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["ContextEngine", "ContextBuilder", "Context", "ContextFile", "TokenBudget"]
|
ctxeng/builder.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Fluent builder API for constructing context step by step."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ctxeng.core import ContextEngine
|
|
8
|
+
from ctxeng.models import Context, TokenBudget
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContextBuilder:
|
|
12
|
+
"""
|
|
13
|
+
Fluent, chainable API for building context.
|
|
14
|
+
|
|
15
|
+
Example::
|
|
16
|
+
|
|
17
|
+
from ctxeng import ContextBuilder
|
|
18
|
+
|
|
19
|
+
ctx = (
|
|
20
|
+
ContextBuilder(root=".")
|
|
21
|
+
.for_model("gpt-4o")
|
|
22
|
+
.only("**/*.py")
|
|
23
|
+
.exclude("tests/**")
|
|
24
|
+
.from_git_diff()
|
|
25
|
+
.with_system("You are a senior Python engineer.")
|
|
26
|
+
.build("Refactor the payment module to use async/await")
|
|
27
|
+
)
|
|
28
|
+
print(ctx.to_string("markdown"))
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, root: str | Path = ".") -> None:
|
|
32
|
+
self._root = Path(root).resolve()
|
|
33
|
+
self._model = "claude-sonnet-4"
|
|
34
|
+
self._budget: TokenBudget | None = None
|
|
35
|
+
self._include: list[str] = []
|
|
36
|
+
self._exclude: list[str] = []
|
|
37
|
+
self._explicit_files: list[Path] = []
|
|
38
|
+
self._use_git_diff = False
|
|
39
|
+
self._git_base = "HEAD"
|
|
40
|
+
self._system = ""
|
|
41
|
+
self._max_file_size_kb = 500
|
|
42
|
+
self._use_git = True
|
|
43
|
+
|
|
44
|
+
def for_model(self, model: str) -> ContextBuilder:
|
|
45
|
+
"""Set the target model (determines token budget)."""
|
|
46
|
+
self._model = model
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def with_budget(self, total: int, reserved_output: int = 2048) -> ContextBuilder:
|
|
50
|
+
"""Set an explicit token budget."""
|
|
51
|
+
self._budget = TokenBudget(total=total, reserved_output=reserved_output)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def only(self, *patterns: str) -> ContextBuilder:
|
|
55
|
+
"""Only include files matching these glob patterns."""
|
|
56
|
+
self._include.extend(patterns)
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def exclude(self, *patterns: str) -> ContextBuilder:
|
|
60
|
+
"""Exclude files matching these glob patterns."""
|
|
61
|
+
self._exclude.extend(patterns)
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def include_files(self, *paths: str | Path) -> ContextBuilder:
|
|
65
|
+
"""Explicitly include specific files (bypasses auto-discovery)."""
|
|
66
|
+
self._explicit_files.extend(Path(p) for p in paths)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def from_git_diff(self, base: str = "HEAD") -> ContextBuilder:
|
|
70
|
+
"""Only include files changed since `base`."""
|
|
71
|
+
self._use_git_diff = True
|
|
72
|
+
self._git_base = base
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def with_system(self, prompt: str) -> ContextBuilder:
|
|
76
|
+
"""Set a system prompt (counts against token budget)."""
|
|
77
|
+
self._system = prompt
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def max_file_size(self, kb: int) -> ContextBuilder:
|
|
81
|
+
"""Skip files larger than this size in KB."""
|
|
82
|
+
self._max_file_size_kb = kb
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def no_git(self) -> ContextBuilder:
|
|
86
|
+
"""Disable git-based recency scoring."""
|
|
87
|
+
self._use_git = False
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def build(self, query: str = "") -> Context:
|
|
91
|
+
"""
|
|
92
|
+
Build and return the optimized Context.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
query: What you want the LLM to do. Be specific for best results.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
:class:`~ctxeng.models.Context`
|
|
99
|
+
"""
|
|
100
|
+
engine = ContextEngine(
|
|
101
|
+
root=self._root,
|
|
102
|
+
model=self._model,
|
|
103
|
+
budget=self._budget,
|
|
104
|
+
max_file_size_kb=self._max_file_size_kb,
|
|
105
|
+
include_patterns=self._include or None,
|
|
106
|
+
exclude_patterns=self._exclude or None,
|
|
107
|
+
use_git=self._use_git,
|
|
108
|
+
)
|
|
109
|
+
return engine.build(
|
|
110
|
+
query=query,
|
|
111
|
+
files=self._explicit_files or None,
|
|
112
|
+
git_diff=self._use_git_diff,
|
|
113
|
+
git_base=self._git_base,
|
|
114
|
+
system_prompt=self._system,
|
|
115
|
+
)
|
ctxeng/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ctxeng CLI — build and inspect context from the command line.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
ctxeng build "Fix the auth bug"
|
|
6
|
+
ctxeng build "Refactor payment module" --model gpt-4o --fmt markdown
|
|
7
|
+
ctxeng build "Review my changes" --git-diff
|
|
8
|
+
ctxeng build "Explain this file" --files src/core.py src/models.py
|
|
9
|
+
ctxeng info
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cmd_build(args: argparse.Namespace) -> None:
|
|
20
|
+
from ctxeng import ContextBuilder
|
|
21
|
+
|
|
22
|
+
builder = (
|
|
23
|
+
ContextBuilder(root=args.root)
|
|
24
|
+
.for_model(args.model)
|
|
25
|
+
.max_file_size(args.max_size)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if args.only:
|
|
29
|
+
builder = builder.only(*args.only)
|
|
30
|
+
if args.exclude:
|
|
31
|
+
builder = builder.exclude(*args.exclude)
|
|
32
|
+
if args.files:
|
|
33
|
+
builder = builder.include_files(*args.files)
|
|
34
|
+
if args.git_diff:
|
|
35
|
+
builder = builder.from_git_diff(args.git_base)
|
|
36
|
+
if args.system:
|
|
37
|
+
builder = builder.with_system(args.system)
|
|
38
|
+
if args.no_git:
|
|
39
|
+
builder = builder.no_git()
|
|
40
|
+
if args.budget:
|
|
41
|
+
builder = builder.with_budget(args.budget)
|
|
42
|
+
|
|
43
|
+
query = " ".join(args.query) if args.query else ""
|
|
44
|
+
|
|
45
|
+
print(f"Building context for: {query!r}", file=sys.stderr)
|
|
46
|
+
print(f"Root: {Path(args.root).resolve()}", file=sys.stderr)
|
|
47
|
+
print(f"Model: {args.model}", file=sys.stderr)
|
|
48
|
+
print("", file=sys.stderr)
|
|
49
|
+
|
|
50
|
+
ctx = builder.build(query)
|
|
51
|
+
|
|
52
|
+
print(ctx.summary(), file=sys.stderr)
|
|
53
|
+
print("", file=sys.stderr)
|
|
54
|
+
print("─" * 60, file=sys.stderr)
|
|
55
|
+
print("Context output:", file=sys.stderr)
|
|
56
|
+
print("─" * 60, file=sys.stderr)
|
|
57
|
+
|
|
58
|
+
if args.output:
|
|
59
|
+
out_path = Path(args.output)
|
|
60
|
+
out_path.write_text(ctx.to_string(args.fmt), encoding="utf-8")
|
|
61
|
+
print(f"Written to: {out_path}", file=sys.stderr)
|
|
62
|
+
else:
|
|
63
|
+
print(ctx.to_string(args.fmt))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_info(args: argparse.Namespace) -> None:
|
|
67
|
+
import subprocess
|
|
68
|
+
|
|
69
|
+
from ctxeng.optimizer import count_tokens, detect_language
|
|
70
|
+
from ctxeng.sources import collect_filesystem
|
|
71
|
+
|
|
72
|
+
root = Path(args.root).resolve()
|
|
73
|
+
print(f"Project: {root}")
|
|
74
|
+
|
|
75
|
+
# Git info
|
|
76
|
+
try:
|
|
77
|
+
r = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
78
|
+
cwd=root, capture_output=True, text=True, timeout=2)
|
|
79
|
+
if r.returncode == 0:
|
|
80
|
+
print(f"Branch: {r.stdout.strip()}")
|
|
81
|
+
r2 = subprocess.run(["git", "log", "-1", "--format=%h %s"],
|
|
82
|
+
cwd=root, capture_output=True, text=True, timeout=2)
|
|
83
|
+
if r2.returncode == 0:
|
|
84
|
+
print(f"Commit: {r2.stdout.strip()}")
|
|
85
|
+
except Exception:
|
|
86
|
+
print("Git: not available")
|
|
87
|
+
|
|
88
|
+
print()
|
|
89
|
+
files = list(collect_filesystem(root))
|
|
90
|
+
lang_counts: dict[str, int] = {}
|
|
91
|
+
total_tokens = 0
|
|
92
|
+
for path, content in files:
|
|
93
|
+
lang = detect_language(path) or "other"
|
|
94
|
+
lang_counts[lang] = lang_counts.get(lang, 0) + 1
|
|
95
|
+
total_tokens += count_tokens(content)
|
|
96
|
+
|
|
97
|
+
print(f"Files found: {len(files)}")
|
|
98
|
+
print(f"Estimated total tokens: {total_tokens:,}")
|
|
99
|
+
print()
|
|
100
|
+
print("By language:")
|
|
101
|
+
for lang, count in sorted(lang_counts.items(), key=lambda x: -x[1])[:10]:
|
|
102
|
+
bar = "█" * min(20, count)
|
|
103
|
+
print(f" {lang:<15} {bar} {count}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
parser = argparse.ArgumentParser(
|
|
108
|
+
prog="ctxeng",
|
|
109
|
+
description="Build perfect LLM context from your codebase.",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument("--root", default=".", help="Project root (default: cwd)")
|
|
112
|
+
|
|
113
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
114
|
+
|
|
115
|
+
# --- build ---
|
|
116
|
+
build_p = sub.add_parser("build", help="Build context for a query")
|
|
117
|
+
build_p.add_argument("query", nargs="*", help="What you want the LLM to do")
|
|
118
|
+
build_p.add_argument("--model", "-m", default="claude-sonnet-4",
|
|
119
|
+
help="Target model (default: claude-sonnet-4)")
|
|
120
|
+
build_p.add_argument("--fmt", "-f", default="xml",
|
|
121
|
+
choices=["xml", "markdown", "plain"],
|
|
122
|
+
help="Output format (default: xml)")
|
|
123
|
+
build_p.add_argument("--output", "-o", help="Write output to file instead of stdout")
|
|
124
|
+
build_p.add_argument("--only", nargs="+", metavar="PATTERN",
|
|
125
|
+
help='Include only matching globs, e.g. "**/*.py"')
|
|
126
|
+
build_p.add_argument("--exclude", nargs="+", metavar="PATTERN",
|
|
127
|
+
help="Exclude matching globs")
|
|
128
|
+
build_p.add_argument("--files", nargs="+", metavar="FILE",
|
|
129
|
+
help="Explicit list of files to include")
|
|
130
|
+
build_p.add_argument("--git-diff", action="store_true",
|
|
131
|
+
help="Only include git-changed files")
|
|
132
|
+
build_p.add_argument("--git-base", default="HEAD",
|
|
133
|
+
help="Git base ref for diff (default: HEAD)")
|
|
134
|
+
build_p.add_argument("--system", help="System prompt text")
|
|
135
|
+
build_p.add_argument("--no-git", action="store_true",
|
|
136
|
+
help="Disable git recency scoring")
|
|
137
|
+
build_p.add_argument("--budget", type=int,
|
|
138
|
+
help="Override token budget total")
|
|
139
|
+
build_p.add_argument("--max-size", type=int, default=500,
|
|
140
|
+
help="Max file size in KB (default: 500)")
|
|
141
|
+
build_p.set_defaults(func=cmd_build)
|
|
142
|
+
|
|
143
|
+
# --- info ---
|
|
144
|
+
info_p = sub.add_parser("info", help="Show project info and file stats")
|
|
145
|
+
info_p.set_defaults(func=cmd_info)
|
|
146
|
+
|
|
147
|
+
args = parser.parse_args()
|
|
148
|
+
args.func(args)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main()
|
ctxeng/core.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Main ContextEngine — the primary public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ctxeng.models import Context, ContextFile, TokenBudget
|
|
8
|
+
from ctxeng.optimizer import count_tokens, detect_language, optimize_budget
|
|
9
|
+
from ctxeng.scorer import rank_files
|
|
10
|
+
from ctxeng.sources import collect_explicit, collect_filesystem, collect_git_changed
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextEngine:
|
|
14
|
+
"""
|
|
15
|
+
Build perfect LLM context from your codebase.
|
|
16
|
+
|
|
17
|
+
Example::
|
|
18
|
+
|
|
19
|
+
from ctxeng import ContextEngine
|
|
20
|
+
|
|
21
|
+
engine = ContextEngine(root=".", model="claude-sonnet-4")
|
|
22
|
+
ctx = engine.build("Fix the authentication bug in the login flow")
|
|
23
|
+
print(ctx.summary())
|
|
24
|
+
# → paste ctx.to_string() into your LLM
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
root: Root directory of your project (default: cwd).
|
|
28
|
+
model: Target model name — used to set token budget.
|
|
29
|
+
budget: Explicit TokenBudget, overrides `model`.
|
|
30
|
+
max_file_size_kb: Skip files larger than this (default: 500 KB).
|
|
31
|
+
include_patterns: Only include files matching these glob patterns.
|
|
32
|
+
exclude_patterns: Skip files matching these glob patterns.
|
|
33
|
+
use_git: Score files using git recency signal (default: True).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
root: str | Path = ".",
|
|
39
|
+
model: str = "claude-sonnet-4",
|
|
40
|
+
budget: TokenBudget | None = None,
|
|
41
|
+
max_file_size_kb: int = 500,
|
|
42
|
+
include_patterns: list[str] | None = None,
|
|
43
|
+
exclude_patterns: list[str] | None = None,
|
|
44
|
+
use_git: bool = True,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.root = Path(root).resolve()
|
|
47
|
+
self.model = model
|
|
48
|
+
self.budget = budget or TokenBudget.for_model(model)
|
|
49
|
+
self.max_file_size_kb = max_file_size_kb
|
|
50
|
+
self.include_patterns = include_patterns
|
|
51
|
+
self.exclude_patterns = exclude_patterns
|
|
52
|
+
self.use_git = use_git
|
|
53
|
+
|
|
54
|
+
def build(
|
|
55
|
+
self,
|
|
56
|
+
query: str = "",
|
|
57
|
+
*,
|
|
58
|
+
files: list[str | Path] | None = None,
|
|
59
|
+
git_diff: bool = False,
|
|
60
|
+
git_base: str = "HEAD",
|
|
61
|
+
system_prompt: str = "",
|
|
62
|
+
fmt: str = "xml",
|
|
63
|
+
) -> Context:
|
|
64
|
+
"""
|
|
65
|
+
Build and return a Context object optimized for your query.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
query: What you're asking the LLM to do. More specific = better scoring.
|
|
69
|
+
files: Explicit list of files to include (bypasses auto-discovery).
|
|
70
|
+
git_diff: Only include files changed since `git_base`.
|
|
71
|
+
git_base: Git ref for diff base (default: HEAD).
|
|
72
|
+
system_prompt: Optional system prompt (counts against token budget).
|
|
73
|
+
fmt: Output format hint, passed through to Context.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A :class:`Context` object. Call ``.to_string()`` to get the LLM-ready string,
|
|
77
|
+
or ``.summary()`` to see what was included and why.
|
|
78
|
+
"""
|
|
79
|
+
# 1. Collect raw files
|
|
80
|
+
raw: list[tuple[Path, str]] = []
|
|
81
|
+
|
|
82
|
+
if files:
|
|
83
|
+
explicit_paths = [Path(f) for f in files]
|
|
84
|
+
raw.extend(collect_explicit(explicit_paths, self.root))
|
|
85
|
+
elif git_diff:
|
|
86
|
+
raw.extend(collect_git_changed(self.root, base=git_base))
|
|
87
|
+
else:
|
|
88
|
+
raw.extend(
|
|
89
|
+
collect_filesystem(
|
|
90
|
+
self.root,
|
|
91
|
+
max_file_size_kb=self.max_file_size_kb,
|
|
92
|
+
include_patterns=self.include_patterns,
|
|
93
|
+
exclude_patterns=self.exclude_patterns,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 2. Score and rank
|
|
98
|
+
ranked = rank_files(raw, query, self.root)
|
|
99
|
+
|
|
100
|
+
# 3. Build ContextFile objects
|
|
101
|
+
context_files = [
|
|
102
|
+
ContextFile(
|
|
103
|
+
path=path,
|
|
104
|
+
content=content,
|
|
105
|
+
relevance_score=score,
|
|
106
|
+
language=detect_language(path),
|
|
107
|
+
)
|
|
108
|
+
for path, content, score in ranked
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# 4. Optimize for token budget
|
|
112
|
+
query_tokens = count_tokens(query, self.model) if query else 0
|
|
113
|
+
system_tokens = count_tokens(system_prompt, self.model) if system_prompt else 0
|
|
114
|
+
|
|
115
|
+
included, skipped = optimize_budget(
|
|
116
|
+
context_files,
|
|
117
|
+
self.budget,
|
|
118
|
+
query_tokens=query_tokens,
|
|
119
|
+
system_tokens=system_tokens,
|
|
120
|
+
model=self.model,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
total_tokens = sum(f.token_count for f in included) + query_tokens + system_tokens
|
|
124
|
+
|
|
125
|
+
# 5. Gather metadata
|
|
126
|
+
metadata = self._gather_metadata()
|
|
127
|
+
|
|
128
|
+
return Context(
|
|
129
|
+
files=included,
|
|
130
|
+
system_prompt=system_prompt,
|
|
131
|
+
query=query,
|
|
132
|
+
total_tokens=total_tokens,
|
|
133
|
+
budget=self.budget,
|
|
134
|
+
skipped_files=skipped,
|
|
135
|
+
metadata=metadata,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _gather_metadata(self) -> dict:
|
|
139
|
+
meta: dict = {"project_root": str(self.root)}
|
|
140
|
+
try:
|
|
141
|
+
import subprocess
|
|
142
|
+
r = subprocess.run(
|
|
143
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
144
|
+
cwd=self.root, capture_output=True, text=True, timeout=2
|
|
145
|
+
)
|
|
146
|
+
if r.returncode == 0:
|
|
147
|
+
meta["git_branch"] = r.stdout.strip()
|
|
148
|
+
r2 = subprocess.run(
|
|
149
|
+
["git", "log", "-1", "--format=%h %s"],
|
|
150
|
+
cwd=self.root, capture_output=True, text=True, timeout=2
|
|
151
|
+
)
|
|
152
|
+
if r2.returncode == 0:
|
|
153
|
+
meta["last_commit"] = r2.stdout.strip()
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return meta
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Drop-in integrations for popular LLM clients.
|
|
3
|
+
|
|
4
|
+
These helpers take a Context object and call the LLM directly,
|
|
5
|
+
so you never have to manually paste context again.
|
|
6
|
+
|
|
7
|
+
Example (Claude)::
|
|
8
|
+
|
|
9
|
+
from ctxeng import ContextEngine
|
|
10
|
+
from ctxeng.integrations import ask_claude
|
|
11
|
+
|
|
12
|
+
engine = ContextEngine(".", model="claude-sonnet-4")
|
|
13
|
+
ctx = engine.build("Why is the login test failing?")
|
|
14
|
+
response = ask_claude(ctx)
|
|
15
|
+
print(response)
|
|
16
|
+
|
|
17
|
+
Example (OpenAI)::
|
|
18
|
+
|
|
19
|
+
from ctxeng import ContextEngine
|
|
20
|
+
from ctxeng.integrations import ask_openai
|
|
21
|
+
|
|
22
|
+
engine = ContextEngine(".", model="gpt-4o")
|
|
23
|
+
ctx = engine.build("Refactor this to be async")
|
|
24
|
+
response = ask_openai(ctx)
|
|
25
|
+
print(response)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ctxeng.models import Context
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ask_claude(
|
|
37
|
+
ctx: Context,
|
|
38
|
+
*,
|
|
39
|
+
api_key: str | None = None,
|
|
40
|
+
model: str | None = None,
|
|
41
|
+
max_tokens: int = 4096,
|
|
42
|
+
fmt: str = "xml",
|
|
43
|
+
) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Send a Context to Anthropic's Claude API and return the response text.
|
|
46
|
+
|
|
47
|
+
Requires: ``pip install anthropic``
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
ctx: The context built by ContextEngine or ContextBuilder.
|
|
51
|
+
api_key: Anthropic API key. Falls back to ANTHROPIC_API_KEY env var.
|
|
52
|
+
model: Model to use. Defaults to claude-sonnet-4-20250514.
|
|
53
|
+
max_tokens: Max response tokens (default 4096).
|
|
54
|
+
fmt: Context rendering format ('xml' recommended for Claude).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The assistant's response as a string.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
import anthropic
|
|
61
|
+
except ImportError:
|
|
62
|
+
raise ImportError(
|
|
63
|
+
"anthropic package required: pip install anthropic"
|
|
64
|
+
) from None
|
|
65
|
+
|
|
66
|
+
client = anthropic.Anthropic(api_key=api_key) if api_key else anthropic.Anthropic()
|
|
67
|
+
model = model or "claude-sonnet-4-20250514"
|
|
68
|
+
|
|
69
|
+
messages = [{"role": "user", "content": ctx.to_string(fmt)}]
|
|
70
|
+
|
|
71
|
+
kwargs: dict = dict(model=model, max_tokens=max_tokens, messages=messages)
|
|
72
|
+
if ctx.system_prompt:
|
|
73
|
+
kwargs["system"] = ctx.system_prompt
|
|
74
|
+
|
|
75
|
+
response = client.messages.create(**kwargs)
|
|
76
|
+
return response.content[0].text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ask_openai(
|
|
80
|
+
ctx: Context,
|
|
81
|
+
*,
|
|
82
|
+
api_key: str | None = None,
|
|
83
|
+
model: str | None = None,
|
|
84
|
+
max_tokens: int = 4096,
|
|
85
|
+
fmt: str = "markdown",
|
|
86
|
+
) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Send a Context to the OpenAI API and return the response text.
|
|
89
|
+
|
|
90
|
+
Requires: ``pip install openai``
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
ctx: The context built by ContextEngine or ContextBuilder.
|
|
94
|
+
api_key: OpenAI API key. Falls back to OPENAI_API_KEY env var.
|
|
95
|
+
model: Model to use. Defaults to gpt-4o.
|
|
96
|
+
max_tokens: Max response tokens (default 4096).
|
|
97
|
+
fmt: Context rendering format ('markdown' recommended for OpenAI).
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The assistant's response as a string.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
from openai import OpenAI
|
|
104
|
+
except ImportError:
|
|
105
|
+
raise ImportError(
|
|
106
|
+
"openai package required: pip install openai"
|
|
107
|
+
) from None
|
|
108
|
+
|
|
109
|
+
client = OpenAI(api_key=api_key) if api_key else OpenAI()
|
|
110
|
+
model = model or "gpt-4o"
|
|
111
|
+
|
|
112
|
+
messages = []
|
|
113
|
+
if ctx.system_prompt:
|
|
114
|
+
messages.append({"role": "system", "content": ctx.system_prompt})
|
|
115
|
+
messages.append({"role": "user", "content": ctx.to_string(fmt)})
|
|
116
|
+
|
|
117
|
+
response = client.chat.completions.create(
|
|
118
|
+
model=model,
|
|
119
|
+
max_tokens=max_tokens,
|
|
120
|
+
messages=messages,
|
|
121
|
+
)
|
|
122
|
+
return response.choices[0].message.content or ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def to_langchain_messages(ctx: Context, fmt: str = "xml") -> list:
|
|
126
|
+
"""
|
|
127
|
+
Convert a Context to a list of LangChain message objects.
|
|
128
|
+
|
|
129
|
+
Requires: ``pip install langchain-core``
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of [SystemMessage?, HumanMessage] suitable for any LangChain chat model.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
136
|
+
except ImportError:
|
|
137
|
+
raise ImportError(
|
|
138
|
+
"langchain-core required: pip install langchain-core"
|
|
139
|
+
) from None
|
|
140
|
+
|
|
141
|
+
messages = []
|
|
142
|
+
if ctx.system_prompt:
|
|
143
|
+
messages.append(SystemMessage(content=ctx.system_prompt))
|
|
144
|
+
messages.append(HumanMessage(content=ctx.to_string(fmt)))
|
|
145
|
+
return messages
|