recon-patent 0.2.0__tar.gz
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.
- recon_patent-0.2.0/LICENSE +21 -0
- recon_patent-0.2.0/PKG-INFO +232 -0
- recon_patent-0.2.0/README.md +189 -0
- recon_patent-0.2.0/cli/__init__.py +0 -0
- recon_patent-0.2.0/cli/download.py +35 -0
- recon_patent-0.2.0/cli/export.py +165 -0
- recon_patent-0.2.0/cli/main.py +305 -0
- recon_patent-0.2.0/clients/__init__.py +0 -0
- recon_patent-0.2.0/clients/base.py +56 -0
- recon_patent-0.2.0/clients/intelligence.py +101 -0
- recon_patent-0.2.0/clients/patent_apis.py +355 -0
- recon_patent-0.2.0/clients/scrapers.py +384 -0
- recon_patent-0.2.0/core/__init__.py +0 -0
- recon_patent-0.2.0/core/arbitrage.py +41 -0
- recon_patent-0.2.0/core/citations.py +197 -0
- recon_patent-0.2.0/core/config.py +87 -0
- recon_patent-0.2.0/core/intelligence.py +58 -0
- recon_patent-0.2.0/core/models.py +42 -0
- recon_patent-0.2.0/core/scoring.py +94 -0
- recon_patent-0.2.0/core/search.py +82 -0
- recon_patent-0.2.0/core/translation.py +126 -0
- recon_patent-0.2.0/pyproject.toml +62 -0
- recon_patent-0.2.0/recon_patent.egg-info/PKG-INFO +232 -0
- recon_patent-0.2.0/recon_patent.egg-info/SOURCES.txt +68 -0
- recon_patent-0.2.0/recon_patent.egg-info/dependency_links.txt +1 -0
- recon_patent-0.2.0/recon_patent.egg-info/entry_points.txt +2 -0
- recon_patent-0.2.0/recon_patent.egg-info/requires.txt +21 -0
- recon_patent-0.2.0/recon_patent.egg-info/top_level.txt +5 -0
- recon_patent-0.2.0/setup.cfg +4 -0
- recon_patent-0.2.0/storage/__init__.py +0 -0
- recon_patent-0.2.0/storage/cache.py +131 -0
- recon_patent-0.2.0/tests/test_arbitrage.py +44 -0
- recon_patent-0.2.0/tests/test_cache.py +53 -0
- recon_patent-0.2.0/tests/test_cache_validation.py +617 -0
- recon_patent-0.2.0/tests/test_citations.py +158 -0
- recon_patent-0.2.0/tests/test_claims_lazy_load.py +42 -0
- recon_patent-0.2.0/tests/test_cli_run.py +43 -0
- recon_patent-0.2.0/tests/test_client.py +25 -0
- recon_patent-0.2.0/tests/test_error_handling.py +926 -0
- recon_patent-0.2.0/tests/test_error_voice.py +55 -0
- recon_patent-0.2.0/tests/test_export.py +75 -0
- recon_patent-0.2.0/tests/test_imports.py +97 -0
- recon_patent-0.2.0/tests/test_integration_new.py +61 -0
- recon_patent-0.2.0/tests/test_intelligence.py +59 -0
- recon_patent-0.2.0/tests/test_lazy_loading.py +32 -0
- recon_patent-0.2.0/tests/test_models.py +21 -0
- recon_patent-0.2.0/tests/test_module_entrypoint.py +15 -0
- recon_patent-0.2.0/tests/test_patent_apis.py +94 -0
- recon_patent-0.2.0/tests/test_performance.py +964 -0
- recon_patent-0.2.0/tests/test_score_algorithm.py +129 -0
- recon_patent-0.2.0/tests/test_scoring.py +99 -0
- recon_patent-0.2.0/tests/test_search.py +97 -0
- recon_patent-0.2.0/tests/test_security.py +76 -0
- recon_patent-0.2.0/tests/test_source_filter.py +104 -0
- recon_patent-0.2.0/tests/test_tab_integration.py +180 -0
- recon_patent-0.2.0/tests/test_terminal_detection.py +31 -0
- recon_patent-0.2.0/tests/test_terminal_protocols.py +20 -0
- recon_patent-0.2.0/tests/test_translation.py +113 -0
- recon_patent-0.2.0/tests/test_tui_components.py +167 -0
- recon_patent-0.2.0/tests/test_tui_layout.py +128 -0
- recon_patent-0.2.0/tests/test_tui_navigation.py +120 -0
- recon_patent-0.2.0/tui/__init__.py +0 -0
- recon_patent-0.2.0/tui/app.py +221 -0
- recon_patent-0.2.0/tui/screens.py +978 -0
- recon_patent-0.2.0/tui/widgets/__init__.py +0 -0
- recon_patent-0.2.0/tui/widgets/citation_tree.py +76 -0
- recon_patent-0.2.0/tui/widgets/claims_tab.py +56 -0
- recon_patent-0.2.0/tui/widgets/image_tab.py +171 -0
- recon_patent-0.2.0/tui/widgets/info_tab.py +94 -0
- recon_patent-0.2.0/tui/widgets/result_list.py +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 RECON Contributors
|
|
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.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: recon-patent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Terminal-native patent research CLI/TUI tool
|
|
5
|
+
Author-email: Anubhav Anand <anubhav@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anubhavanand/recon
|
|
8
|
+
Project-URL: Repository, https://github.com/anubhavanand/recon
|
|
9
|
+
Project-URL: Documentation, https://github.com/anubhavanand/recon#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/anubhavanand/recon/issues
|
|
11
|
+
Keywords: patent,research,cli,tui,textual
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: textual>=1.0
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: Pillow>=10.0
|
|
26
|
+
Requires-Dist: rapidfuzz>=3.0
|
|
27
|
+
Requires-Dist: typer>=0.9
|
|
28
|
+
Requires-Dist: fpdf2>=2.7
|
|
29
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
30
|
+
Requires-Dist: lxml>=5.0
|
|
31
|
+
Requires-Dist: ddgs>=1.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
36
|
+
Requires-Dist: psutil>=5.9; extra == "test"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
41
|
+
Requires-Dist: psutil>=5.9; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# RECON — Terminal-Native Patent Research Tool
|
|
45
|
+
|
|
46
|
+
**RECON** is a keyboard-first, terminal-native patent research tool. It aggregates patent data from multiple sources (USPTO, PatSnap, Google Patents, WIPO, Lens.org, EPO), presents results in a clean TUI or CLI, and provides scoring, citation graphs, export, and optional local-AI translation.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Multi-source search** — USPTO API, PatSnap API, Google Patents scraping, WIPO/Lens/EPO via DuckDuckGo + BeautifulSoup
|
|
51
|
+
- **Source filtering** — Include/exclude sources via CLI `--source` or TUI `S` overlay
|
|
52
|
+
- **Signal scoring** — Equal-weight 20-point signals (government grants, corporate investment, academic research, temporal recency, news/media)
|
|
53
|
+
- **Live preview** — Three-tab detail pane (Info / Claims / Image) with keyboard navigation
|
|
54
|
+
- **Citation graph** — ASCII tree view of forward/backward citations (scraped from Google Patents or mock data)
|
|
55
|
+
- **Export** — JSON, CSV, BibTeX, Markdown, PDF
|
|
56
|
+
- **Translation** — Optional local translation via Ollama (opt-in, Zero-AI default)
|
|
57
|
+
- **Terminal-native** — No GUI, no Electron, no modal dialogs
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
### Via PyPI (recommended)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pipx install recon-patent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Via GitHub
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pipx install git+https://github.com/anubhavaanand/recon.git
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Ensure `~/.local/bin` is in your `PATH`.
|
|
74
|
+
|
|
75
|
+
### Via install script (hacker method)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -sSL https://raw.githubusercontent.com/anubhavaanand/recon/main/install.sh | bash
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Via pip
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install recon-patent
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### From source
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/anubhavaanand/recon.git
|
|
91
|
+
cd recon
|
|
92
|
+
pip install -e .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install -e ".[test,dev]"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Quick Start
|
|
102
|
+
|
|
103
|
+
### Launch the interactive TUI
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
recon
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Type a query (e.g. `solid state battery`) and press Enter. Navigate results with `j`/`k`, open detail with Enter, switch tabs with `h`/`l`.
|
|
110
|
+
|
|
111
|
+
### CLI search
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
recon search "sulfide electrolyte"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Filter by source:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
recon search "quantum battery" --source uspto,patsnap,google
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### End-to-end run with export
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
recon run "lithium anode" --export json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### View saved collection
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
recon collection list
|
|
133
|
+
recon collection clear
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Manage API keys
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
recon config show
|
|
140
|
+
recon config set --patsnap-key YOUR_KEY
|
|
141
|
+
recon config test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Export collection
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
recon export --format csv
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Formats: `json`, `csv`, `bibtex`, `markdown`, `pdf`.
|
|
151
|
+
|
|
152
|
+
## TUI Key Bindings
|
|
153
|
+
|
|
154
|
+
| Key | Action |
|
|
155
|
+
|-----|--------|
|
|
156
|
+
| `↑`/`↓` `j`/`k` | Navigate results |
|
|
157
|
+
| `Enter` | Open detail view |
|
|
158
|
+
| `h`/`l` or `←`/`→` | Switch preview tab |
|
|
159
|
+
| `/` | Focus search input |
|
|
160
|
+
| `s` | Save patent to collection |
|
|
161
|
+
| `e` | Export collection overlay |
|
|
162
|
+
| `S` | Source filter overlay |
|
|
163
|
+
| `c` | Toggle citation graph |
|
|
164
|
+
| `t` | Toggle translation |
|
|
165
|
+
| `r` | Reader mode |
|
|
166
|
+
| `m` | Synthesis mode |
|
|
167
|
+
| `?` | Help overlay |
|
|
168
|
+
| `q` / `Esc` | Back / Quit |
|
|
169
|
+
|
|
170
|
+
## Architecture
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
recon/
|
|
174
|
+
├── cli/main.py — Typer CLI (search, run, config, export, collection)
|
|
175
|
+
├── core/
|
|
176
|
+
│ ├── models.py — PatentRecord, CrossReference dataclasses
|
|
177
|
+
│ ├── search.py — Multi-source search orchestration + source filtering
|
|
178
|
+
│ ├── scoring.py — Signal scoring (equal-weight algorithm)
|
|
179
|
+
│ ├── arbitrage.py — Arbitrage status calculation
|
|
180
|
+
│ ├── citations.py — Citation graph fetching (Google Patents scrape + mock)
|
|
181
|
+
│ ├── translation.py — Local Ollama translation with cache
|
|
182
|
+
│ └── config.py — Config management (.env + JSON)
|
|
183
|
+
├── clients/
|
|
184
|
+
│ ├── patent_apis.py — USPTO, PatSnap, Google, WIPO, Lens, EPO clients
|
|
185
|
+
│ ├── scrapers.py — DDGS + BeautifulSoup scrapers (WIPO, Lens, EPO, Google)
|
|
186
|
+
│ └── base.py — BaseAsyncClient with rate-limit + backoff
|
|
187
|
+
├── tui/
|
|
188
|
+
│ ├── app.py — Textual App + CSS
|
|
189
|
+
│ ├── screens.py — SearchScreen, DetailScreen, ReaderModeScreen, etc.
|
|
190
|
+
│ └── widgets/ — ResultList, InfoTab, ClaimsTab, ImageTab, CitationTree
|
|
191
|
+
├── storage/
|
|
192
|
+
│ └── cache.py — SQLite cache (search results, collection, translations)
|
|
193
|
+
└── tests/ — 200+ tests (pytest)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Configuration
|
|
197
|
+
|
|
198
|
+
Keys are stored in `~/.config/recon/config.json` (chmod 600):
|
|
199
|
+
|
|
200
|
+
| Variable | Source | Required |
|
|
201
|
+
|----------|--------|----------|
|
|
202
|
+
| `PATSNAP_API_KEY` | PatSnap | No (scraper fallback exists for most features) |
|
|
203
|
+
| `USPTO_API_KEY` | USPTO | No |
|
|
204
|
+
| `EPO_CONSUMER_KEY` / `EPO_CONSUMER_SECRET` | EPO | No (scraper-only) |
|
|
205
|
+
| `LENS_API_KEY` | Lens.org | No (scraper-only) |
|
|
206
|
+
|
|
207
|
+
Set via `recon config set` (interactive) or `.env` file.
|
|
208
|
+
|
|
209
|
+
## Translation (Ollama)
|
|
210
|
+
|
|
211
|
+
RECON can optionally translate non-English patent abstracts using a local Ollama instance:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Install Ollama: https://ollama.ai
|
|
215
|
+
ollama pull llama3
|
|
216
|
+
|
|
217
|
+
# RECON auto-detects non-English text and uses Ollama
|
|
218
|
+
# Press t in the TUI to toggle translation
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
When Ollama is not running, RECON gracefully displays the original text.
|
|
222
|
+
|
|
223
|
+
## Testing
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
pip install -e ".[test]"
|
|
227
|
+
pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# RECON — Terminal-Native Patent Research Tool
|
|
2
|
+
|
|
3
|
+
**RECON** is a keyboard-first, terminal-native patent research tool. It aggregates patent data from multiple sources (USPTO, PatSnap, Google Patents, WIPO, Lens.org, EPO), presents results in a clean TUI or CLI, and provides scoring, citation graphs, export, and optional local-AI translation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-source search** — USPTO API, PatSnap API, Google Patents scraping, WIPO/Lens/EPO via DuckDuckGo + BeautifulSoup
|
|
8
|
+
- **Source filtering** — Include/exclude sources via CLI `--source` or TUI `S` overlay
|
|
9
|
+
- **Signal scoring** — Equal-weight 20-point signals (government grants, corporate investment, academic research, temporal recency, news/media)
|
|
10
|
+
- **Live preview** — Three-tab detail pane (Info / Claims / Image) with keyboard navigation
|
|
11
|
+
- **Citation graph** — ASCII tree view of forward/backward citations (scraped from Google Patents or mock data)
|
|
12
|
+
- **Export** — JSON, CSV, BibTeX, Markdown, PDF
|
|
13
|
+
- **Translation** — Optional local translation via Ollama (opt-in, Zero-AI default)
|
|
14
|
+
- **Terminal-native** — No GUI, no Electron, no modal dialogs
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Via PyPI (recommended)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pipx install recon-patent
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Via GitHub
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pipx install git+https://github.com/anubhavaanand/recon.git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Ensure `~/.local/bin` is in your `PATH`.
|
|
31
|
+
|
|
32
|
+
### Via install script (hacker method)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -sSL https://raw.githubusercontent.com/anubhavaanand/recon/main/install.sh | bash
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Via pip
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install recon-patent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### From source
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/anubhavaanand/recon.git
|
|
48
|
+
cd recon
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e ".[test,dev]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Launch the interactive TUI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
recon
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Type a query (e.g. `solid state battery`) and press Enter. Navigate results with `j`/`k`, open detail with Enter, switch tabs with `h`/`l`.
|
|
67
|
+
|
|
68
|
+
### CLI search
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
recon search "sulfide electrolyte"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Filter by source:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
recon search "quantum battery" --source uspto,patsnap,google
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### End-to-end run with export
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
recon run "lithium anode" --export json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### View saved collection
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
recon collection list
|
|
90
|
+
recon collection clear
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Manage API keys
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
recon config show
|
|
97
|
+
recon config set --patsnap-key YOUR_KEY
|
|
98
|
+
recon config test
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Export collection
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
recon export --format csv
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Formats: `json`, `csv`, `bibtex`, `markdown`, `pdf`.
|
|
108
|
+
|
|
109
|
+
## TUI Key Bindings
|
|
110
|
+
|
|
111
|
+
| Key | Action |
|
|
112
|
+
|-----|--------|
|
|
113
|
+
| `↑`/`↓` `j`/`k` | Navigate results |
|
|
114
|
+
| `Enter` | Open detail view |
|
|
115
|
+
| `h`/`l` or `←`/`→` | Switch preview tab |
|
|
116
|
+
| `/` | Focus search input |
|
|
117
|
+
| `s` | Save patent to collection |
|
|
118
|
+
| `e` | Export collection overlay |
|
|
119
|
+
| `S` | Source filter overlay |
|
|
120
|
+
| `c` | Toggle citation graph |
|
|
121
|
+
| `t` | Toggle translation |
|
|
122
|
+
| `r` | Reader mode |
|
|
123
|
+
| `m` | Synthesis mode |
|
|
124
|
+
| `?` | Help overlay |
|
|
125
|
+
| `q` / `Esc` | Back / Quit |
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
recon/
|
|
131
|
+
├── cli/main.py — Typer CLI (search, run, config, export, collection)
|
|
132
|
+
├── core/
|
|
133
|
+
│ ├── models.py — PatentRecord, CrossReference dataclasses
|
|
134
|
+
│ ├── search.py — Multi-source search orchestration + source filtering
|
|
135
|
+
│ ├── scoring.py — Signal scoring (equal-weight algorithm)
|
|
136
|
+
│ ├── arbitrage.py — Arbitrage status calculation
|
|
137
|
+
│ ├── citations.py — Citation graph fetching (Google Patents scrape + mock)
|
|
138
|
+
│ ├── translation.py — Local Ollama translation with cache
|
|
139
|
+
│ └── config.py — Config management (.env + JSON)
|
|
140
|
+
├── clients/
|
|
141
|
+
│ ├── patent_apis.py — USPTO, PatSnap, Google, WIPO, Lens, EPO clients
|
|
142
|
+
│ ├── scrapers.py — DDGS + BeautifulSoup scrapers (WIPO, Lens, EPO, Google)
|
|
143
|
+
│ └── base.py — BaseAsyncClient with rate-limit + backoff
|
|
144
|
+
├── tui/
|
|
145
|
+
│ ├── app.py — Textual App + CSS
|
|
146
|
+
│ ├── screens.py — SearchScreen, DetailScreen, ReaderModeScreen, etc.
|
|
147
|
+
│ └── widgets/ — ResultList, InfoTab, ClaimsTab, ImageTab, CitationTree
|
|
148
|
+
├── storage/
|
|
149
|
+
│ └── cache.py — SQLite cache (search results, collection, translations)
|
|
150
|
+
└── tests/ — 200+ tests (pytest)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
Keys are stored in `~/.config/recon/config.json` (chmod 600):
|
|
156
|
+
|
|
157
|
+
| Variable | Source | Required |
|
|
158
|
+
|----------|--------|----------|
|
|
159
|
+
| `PATSNAP_API_KEY` | PatSnap | No (scraper fallback exists for most features) |
|
|
160
|
+
| `USPTO_API_KEY` | USPTO | No |
|
|
161
|
+
| `EPO_CONSUMER_KEY` / `EPO_CONSUMER_SECRET` | EPO | No (scraper-only) |
|
|
162
|
+
| `LENS_API_KEY` | Lens.org | No (scraper-only) |
|
|
163
|
+
|
|
164
|
+
Set via `recon config set` (interactive) or `.env` file.
|
|
165
|
+
|
|
166
|
+
## Translation (Ollama)
|
|
167
|
+
|
|
168
|
+
RECON can optionally translate non-English patent abstracts using a local Ollama instance:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Install Ollama: https://ollama.ai
|
|
172
|
+
ollama pull llama3
|
|
173
|
+
|
|
174
|
+
# RECON auto-detects non-English text and uses Ollama
|
|
175
|
+
# Press t in the TUI to toggle translation
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
When Ollama is not running, RECON gracefully displays the original text.
|
|
179
|
+
|
|
180
|
+
## Testing
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e ".[test]"
|
|
184
|
+
pytest
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from core.models import PatentRecord
|
|
5
|
+
|
|
6
|
+
async def download_patent_assets(record: PatentRecord, base_path: str = "downloads"):
|
|
7
|
+
"""
|
|
8
|
+
Download patent figures and metadata for offline archival.
|
|
9
|
+
PRD §3.5 — Download (key: d).
|
|
10
|
+
"""
|
|
11
|
+
target_dir = Path(base_path) / record.id
|
|
12
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
# 1. Save metadata as JSON
|
|
15
|
+
import json
|
|
16
|
+
import dataclasses
|
|
17
|
+
meta_path = target_dir / "metadata.json"
|
|
18
|
+
with open(meta_path, "w") as f:
|
|
19
|
+
json.dump(dataclasses.asdict(record), f, indent=2)
|
|
20
|
+
|
|
21
|
+
# 2. Download figures
|
|
22
|
+
if record.image_urls:
|
|
23
|
+
async with httpx.AsyncClient(timeout=30.0, trust_env=False) as client:
|
|
24
|
+
for i, url in enumerate(record.image_urls, 1):
|
|
25
|
+
try:
|
|
26
|
+
response = await client.get(url)
|
|
27
|
+
if response.status_code == 200:
|
|
28
|
+
ext = Path(url).suffix or ".jpg"
|
|
29
|
+
img_path = target_dir / f"figure_{i}{ext}"
|
|
30
|
+
with open(img_path, "wb") as f:
|
|
31
|
+
f.write(response.content)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f"ERR: Failed to download figure {i}: {e}")
|
|
34
|
+
|
|
35
|
+
return target_dir
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Any
|
|
5
|
+
from core.models import PatentRecord
|
|
6
|
+
import fpdf
|
|
7
|
+
|
|
8
|
+
def _safe_csv_field(value: str) -> str:
|
|
9
|
+
"""Sanitize a CSV field to prevent formula injection in spreadsheet apps.
|
|
10
|
+
|
|
11
|
+
Leading =, +, -, @ can trigger DDE/formula execution in Excel/Google Sheets.
|
|
12
|
+
Prefix them with a tab to neutralize.
|
|
13
|
+
"""
|
|
14
|
+
value = str(value)
|
|
15
|
+
if value and value[0] in ("=", "+", "-", "@", "\t", "\r"):
|
|
16
|
+
return "\t" + value
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
def _export_csv(records: List[PatentRecord], output_path: str):
|
|
20
|
+
if not records:
|
|
21
|
+
Path(output_path).touch()
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
with open(output_path, mode='w', newline='', encoding='utf-8') as f:
|
|
25
|
+
writer = csv.writer(f)
|
|
26
|
+
writer.writerow(["ID", "Title", "Assignee", "Filed Date", "Status", "Score"])
|
|
27
|
+
from core.scoring import calculate_signal_score
|
|
28
|
+
for record in records:
|
|
29
|
+
score = calculate_signal_score(record.cross_references)
|
|
30
|
+
writer.writerow([
|
|
31
|
+
_safe_csv_field(record.id),
|
|
32
|
+
_safe_csv_field(record.title),
|
|
33
|
+
_safe_csv_field(record.assignee),
|
|
34
|
+
_safe_csv_field(record.dates.get("filed", "")),
|
|
35
|
+
_safe_csv_field(record.status),
|
|
36
|
+
_safe_csv_field(str(score))
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
def _export_json(records: List[PatentRecord], output_path: str):
|
|
40
|
+
import dataclasses
|
|
41
|
+
def _default(o: Any):
|
|
42
|
+
if dataclasses.is_dataclass(o):
|
|
43
|
+
return dataclasses.asdict(o)
|
|
44
|
+
return str(o)
|
|
45
|
+
|
|
46
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
47
|
+
json.dump([_default(r) for r in records], f, indent=2)
|
|
48
|
+
|
|
49
|
+
def _export_bibtex(records: List[PatentRecord], output_path: str):
|
|
50
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
51
|
+
for record in records:
|
|
52
|
+
f.write(f"@misc{{{record.id},\n")
|
|
53
|
+
f.write(f" title = {{{record.title}}},\n")
|
|
54
|
+
f.write(f" author = {{{record.assignee}}},\n")
|
|
55
|
+
f.write(f" year = {{{record.dates.get('filed', '')[:4] if record.dates.get('filed') else ''}}},\n")
|
|
56
|
+
f.write(f" note = {{Status: {record.status}}}\n")
|
|
57
|
+
f.write("}\n\n")
|
|
58
|
+
|
|
59
|
+
def _export_markdown(records: List[PatentRecord], output_path: str):
|
|
60
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
61
|
+
for record in records:
|
|
62
|
+
f.write(f"# {record.title}\n\n")
|
|
63
|
+
f.write(f"**ID**: {record.id} | **Assignee**: {record.assignee} | **Filed**: {record.dates.get('filed', '[?]')}\n\n")
|
|
64
|
+
f.write(f"## Abstract\n{record.abstract}\n\n")
|
|
65
|
+
f.write("---\n\n")
|
|
66
|
+
|
|
67
|
+
def _export_pdf(records: List[PatentRecord], output_path: str):
|
|
68
|
+
"""Generate PDF export with title page and individual patent pages."""
|
|
69
|
+
pdf = fpdf.FPDF()
|
|
70
|
+
pdf.set_auto_page_break(auto=True, margin=15)
|
|
71
|
+
|
|
72
|
+
# Title page
|
|
73
|
+
pdf.add_page()
|
|
74
|
+
pdf.set_font("helvetica", style="B", size=24)
|
|
75
|
+
pdf.ln(80)
|
|
76
|
+
pdf.cell(0, 20, "RECON Export", align="C")
|
|
77
|
+
pdf.ln(15)
|
|
78
|
+
pdf.set_font("helvetica", size=18)
|
|
79
|
+
pdf.cell(0, 20, f"{len(records)} Patents", align="C")
|
|
80
|
+
pdf.set_font("helvetica", size=12)
|
|
81
|
+
pdf.ln(50)
|
|
82
|
+
pdf.cell(0, 10, f"Generated: {Path(output_path).stem}", align="C")
|
|
83
|
+
|
|
84
|
+
# Patent pages
|
|
85
|
+
for record in records:
|
|
86
|
+
pdf.add_page()
|
|
87
|
+
pdf.set_font("helvetica", style="B", size=14)
|
|
88
|
+
safe_title = record.title.encode('latin-1', 'replace').decode('latin-1')
|
|
89
|
+
pdf.multi_cell(w=180, h=8, text=safe_title, align="L")
|
|
90
|
+
pdf.ln(3)
|
|
91
|
+
|
|
92
|
+
# ID, Assignee, Dates row
|
|
93
|
+
pdf.set_font("helvetica", size=10)
|
|
94
|
+
pdf.multi_cell(w=180, h=6, text=f"ID: {record.id}", align="L")
|
|
95
|
+
safe_assignee = record.assignee.encode('latin-1', 'replace').decode('latin-1')
|
|
96
|
+
pdf.multi_cell(w=180, h=6, text=f"Assignee: {safe_assignee}", align="L")
|
|
97
|
+
|
|
98
|
+
# Dates
|
|
99
|
+
dates_str = " | ".join([f"{k}: {v}" for k, v in record.dates.items()])
|
|
100
|
+
safe_dates = dates_str.encode('latin-1', 'replace').decode('latin-1')
|
|
101
|
+
pdf.multi_cell(w=180, h=6, text=safe_dates, align="L")
|
|
102
|
+
pdf.multi_cell(w=180, h=6, text=f"Status: {record.status}", align="L")
|
|
103
|
+
pdf.ln(5)
|
|
104
|
+
|
|
105
|
+
# Abstract
|
|
106
|
+
pdf.set_font("helvetica", style="B", size=11)
|
|
107
|
+
pdf.cell(0, 8, "Abstract", align="L")
|
|
108
|
+
pdf.ln(8)
|
|
109
|
+
pdf.set_font("helvetica", size=10)
|
|
110
|
+
safe_abstract = record.abstract.encode('latin-1', 'replace').decode('latin-1')
|
|
111
|
+
pdf.multi_cell(w=180, h=6, text=safe_abstract, align="L")
|
|
112
|
+
pdf.ln(5)
|
|
113
|
+
|
|
114
|
+
# Claims
|
|
115
|
+
if record.claims:
|
|
116
|
+
pdf.set_font("helvetica", style="B", size=11)
|
|
117
|
+
pdf.cell(0, 8, "Claims", align="L")
|
|
118
|
+
pdf.ln(8)
|
|
119
|
+
pdf.set_font("helvetica", size=10)
|
|
120
|
+
for i, claim in enumerate(record.claims, 1):
|
|
121
|
+
safe_claim = claim.encode('latin-1', 'replace').decode('latin-1')
|
|
122
|
+
claim_text = f"{i}. {safe_claim}"
|
|
123
|
+
pdf.multi_cell(w=180, h=6, text=claim_text, align="L")
|
|
124
|
+
|
|
125
|
+
pdf.output(output_path)
|
|
126
|
+
|
|
127
|
+
def export_records(records: List[PatentRecord], format: str, output_path: str):
|
|
128
|
+
format = format.lower()
|
|
129
|
+
try:
|
|
130
|
+
if format == "csv":
|
|
131
|
+
_export_csv(records, output_path)
|
|
132
|
+
elif format == "json":
|
|
133
|
+
_export_json(records, output_path)
|
|
134
|
+
elif format == "bibtex":
|
|
135
|
+
_export_bibtex(records, output_path)
|
|
136
|
+
elif format == "markdown":
|
|
137
|
+
_export_markdown(records, output_path)
|
|
138
|
+
elif format == "pdf":
|
|
139
|
+
_export_pdf(records, output_path)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"ERR: Export to {format} failed. Reason: {e}")
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
def export_pdf(collection: List[PatentRecord], path: str) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Export patent collection to PDF file.
|
|
149
|
+
Generates a title page followed by one page per patent.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
collection: List of PatentRecord objects to export
|
|
153
|
+
path: Output file path (e.g., '/path/to/export.pdf')
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If collection is empty
|
|
157
|
+
IOError: If file cannot be written
|
|
158
|
+
"""
|
|
159
|
+
if not collection:
|
|
160
|
+
raise ValueError("Cannot export empty collection.")
|
|
161
|
+
try:
|
|
162
|
+
_export_pdf(collection, path)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"ERR: PDF export failed. Reason: {e}")
|
|
165
|
+
raise
|