docseek 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.
docseek/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ _PACKAGE_ROOT = Path(__file__).resolve().parent
6
+ _SERVICE_ENV = _PACKAGE_ROOT.parent / '.env'
7
+
8
+ load_dotenv(_SERVICE_ENV, override=False)
9
+ load_dotenv(override=False)
10
+
11
+ from .models import ANode, AgentAction, FullElement
12
+ from .scraper import fetch_and_build_tree
13
+
14
+ __all__ = [
15
+ 'ANode',
16
+ 'AgentAction',
17
+ 'FullElement',
18
+ 'fetch_and_build_tree',
19
+ ]
docseek/__main__.py ADDED
@@ -0,0 +1,143 @@
1
+ """Command line.
2
+
3
+ docseek URL GOAL [options] the same crawl as POST /v1/discover, printed as JSON
4
+ docseek generate URL GOAL write the site's discovery program (needs PROGRAMS_DIR and a coding model)
5
+ docseek check [KEY ...] replay programs and compare them with their snapshots (no model)
6
+
7
+ Progress goes to stderr.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import time
16
+
17
+
18
+ def _programs_dir_arg(parser: argparse.ArgumentParser) -> None:
19
+ parser.add_argument('--programs-dir', default=os.environ.get('PROGRAMS_DIR'),
20
+ help='Where generated programs live (default: PROGRAMS_DIR)')
21
+
22
+
23
+ def _judge_args(parser: argparse.ArgumentParser) -> None:
24
+ parser.add_argument('--profile', default='generic', help='Domain profile name or path (default: generic)')
25
+ parser.add_argument('--judge', choices=('jev', 'llm'), default=None,
26
+ help='Relevance judge (default: jev when TYPESAFE_API_KEY is set, else llm)')
27
+
28
+
29
+ def _fail(message: str) -> None:
30
+ print(f'docseek: {message}', file=sys.stderr)
31
+ sys.exit(1)
32
+
33
+
34
+ def _discover(argv: list[str]) -> None:
35
+ parser = argparse.ArgumentParser(prog='docseek', description='Find the documents a goal asks for on a website.',
36
+ epilog='Also: docseek generate URL GOAL, docseek check [KEY ...]')
37
+ parser.add_argument('url', help='Start URL for the crawl')
38
+ parser.add_argument('goal', help='What to find, in plain language')
39
+ _judge_args(parser)
40
+ parser.add_argument('--decision-layer', choices=('jev', 'agent'), default=None,
41
+ help='jev: the judge-driven crawl (with any judge); agent: the browsing agent alone')
42
+ parser.add_argument('--model', default=None, help='LLM model override (default: LLM_MODEL)')
43
+ parser.add_argument('--max-pages', type=int, default=10)
44
+ parser.add_argument('--max-seconds', type=float, default=180)
45
+ parser.add_argument('--max-depth', type=int, default=3)
46
+ parser.add_argument('--include-rejected', action='store_true', help='Also print rejected candidates')
47
+ parser.add_argument('--programs', action='store_true',
48
+ help="Answer from the site's generated program when it is healthy; after a crawl, write one")
49
+ _programs_dir_arg(parser)
50
+ args = parser.parse_args(argv)
51
+
52
+ from fastapi import HTTPException
53
+
54
+ from . import server
55
+
56
+ server.PROGRAMS_DIR = args.programs_dir
57
+
58
+ def on_event(ev: dict) -> None:
59
+ if ev.get('type') == 'crawl_page_start':
60
+ print(f' -> {ev.get("url", "")}', file=sys.stderr)
61
+ elif ev.get('type') == 'agent_download':
62
+ print(f' found: {ev.get("url", "")}', file=sys.stderr)
63
+
64
+ payload = server.DiscoverRequest(
65
+ url=args.url, goal=args.goal, profile=args.profile, judge=args.judge, decision_layer=args.decision_layer,
66
+ model=args.model, max_pages=args.max_pages, max_seconds=args.max_seconds, max_depth=args.max_depth,
67
+ include_rejected=args.include_rejected, programs=args.programs,
68
+ )
69
+ try:
70
+ result = server._discover(payload, server._agent_llm(payload), on_event=on_event)
71
+ except HTTPException as exc:
72
+ _fail(exc.detail)
73
+ print(json.dumps(result.model_dump(), indent=2, ensure_ascii=False))
74
+ if (result.program or {}).get('generation') == 'started':
75
+ print('docseek: writing a program for next time (this takes a few minutes)...', file=sys.stderr)
76
+ while server._generating:
77
+ time.sleep(1)
78
+
79
+
80
+ def _generate(argv: list[str]) -> None:
81
+ parser = argparse.ArgumentParser(prog='docseek generate', description="Write a site's discovery program.")
82
+ parser.add_argument('url')
83
+ parser.add_argument('goal')
84
+ _judge_args(parser)
85
+ _programs_dir_arg(parser)
86
+ args = parser.parse_args(argv)
87
+ if not args.programs_dir:
88
+ _fail('set PROGRAMS_DIR or pass --programs-dir')
89
+
90
+ from .codegen.explorer import codegen_llm
91
+ from .codegen.programs import ProgramStore, generate_program
92
+ from .judge import JudgeUnavailable, make_judge
93
+ from .profile import UnknownProfile, load_profile
94
+
95
+ llm = codegen_llm()
96
+ if llm is None:
97
+ _fail('no coding model configured: set CODEGEN_MODEL (or LLM_MODEL) and the provider settings')
98
+ try:
99
+ judge = make_judge(args.judge, load_profile(args.profile))
100
+ except (JudgeUnavailable, UnknownProfile) as exc:
101
+ _fail(str(exc))
102
+ program = generate_program(ProgramStore(args.programs_dir), args.url, args.goal, llm=llm, judge=judge)
103
+ if program is None:
104
+ _fail('the agent produced no program')
105
+ meta = program.meta
106
+ print(json.dumps({'key': program.key, **{k: meta.get(k) for k in (
107
+ 'submitted', 'turns', 'model', 'usage', 'seconds', 'generated_kept', 'data_hosts', 'post_endpoints', 'notes')}},
108
+ indent=2, ensure_ascii=False))
109
+
110
+
111
+ def _check(argv: list[str]) -> None:
112
+ parser = argparse.ArgumentParser(prog='docseek check',
113
+ description='Replay generated programs and compare them with their snapshots.')
114
+ parser.add_argument('keys', nargs='*', help='Program keys (default: every program)')
115
+ _programs_dir_arg(parser)
116
+ args = parser.parse_args(argv)
117
+ if not args.programs_dir:
118
+ _fail('set PROGRAMS_DIR or pass --programs-dir')
119
+
120
+ from .codegen.programs import ProgramStore, check_drift
121
+
122
+ rows = check_drift(ProgramStore(args.programs_dir), args.keys or None)
123
+ for row in rows:
124
+ detail = (f"kept {row['kept']:.0%} {row['then']} -> {row['now']}" if 'kept' in row
125
+ else f"{row['now']} documents")
126
+ print(f"{row['key']:45} {row['status']:8} {detail}" + (f" {row['error'].splitlines()[-1][:80]}"
127
+ if row.get('error') else ''), file=sys.stderr)
128
+ print(json.dumps(rows, indent=2))
129
+ if any(row['status'] in ('broken', 'shrank') for row in rows):
130
+ sys.exit(2)
131
+
132
+
133
+ def main() -> None:
134
+ argv = sys.argv[1:]
135
+ commands = {'generate': _generate, 'check': _check}
136
+ if argv and argv[0] in commands:
137
+ commands[argv[0]](argv[1:])
138
+ else:
139
+ _discover(argv)
140
+
141
+
142
+ if __name__ == '__main__':
143
+ main()