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 +19 -0
- docseek/__main__.py +143 -0
- docseek/agent.py +1980 -0
- docseek/api_mining.py +180 -0
- docseek/codegen/__init__.py +8 -0
- docseek/codegen/explorer.py +326 -0
- docseek/codegen/fetcher.py +217 -0
- docseek/codegen/programs.py +231 -0
- docseek/codegen/sandbox.py +165 -0
- docseek/document_probe.py +116 -0
- docseek/frontier.py +152 -0
- docseek/jev.py +239 -0
- docseek/jev_crawl.py +1116 -0
- docseek/json_mining.py +117 -0
- docseek/judge.py +299 -0
- docseek/learn.py +98 -0
- docseek/llm.py +291 -0
- docseek/models.py +116 -0
- docseek/patterns.py +127 -0
- docseek/profile.py +57 -0
- docseek/profiles/fund-reports.json +15 -0
- docseek/profiles/generic.json +15 -0
- docseek/query.py +9 -0
- docseek/reach.py +177 -0
- docseek/recipes.py +180 -0
- docseek/scraper.py +1117 -0
- docseek/server.py +506 -0
- docseek-0.1.0.dist-info/METADATA +231 -0
- docseek-0.1.0.dist-info/RECORD +32 -0
- docseek-0.1.0.dist-info/WHEEL +4 -0
- docseek-0.1.0.dist-info/entry_points.txt +2 -0
- docseek-0.1.0.dist-info/licenses/LICENSE +202 -0
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()
|