Reddit client complete
This commit is contained in:
commit
b9f0dc1ea4
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Created by venv; see https://docs.python.org/3/library/venv.html
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
target/
|
||||
|
||||
pyvenv.cfg
|
||||
112
README.md
Normal file
112
README.md
Normal file
@ -0,0 +1,112 @@
|
||||
# Reddit CLI
|
||||
|
||||
A command-line interface for browsing Reddit posts with search capabilities, interactive post viewing, and AI-powered summarization.
|
||||
|
||||
## Features
|
||||
|
||||
- **Search Functionality**: Search for Reddit posts by keyword/terms
|
||||
- **Results Display**: Table view showing:
|
||||
- Post number
|
||||
- Subreddit name
|
||||
- Post creation date
|
||||
- Post title (main column)
|
||||
- **Post Navigation**: Click on post number to view full post details
|
||||
- **Comments Viewer**: Cycle through comments using 'c' key
|
||||
- **AI Integration**: Get AI-generated summaries using Ollama API
|
||||
- **Pagination**: Navigate between pages of results ('n' key)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
reddit-cli/
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # Main application entrypoint
|
||||
│ ├── reddit_client.py # Reddit API client implementation
|
||||
│ └── ai_client.py # Ollama AI integration
|
||||
├── tests/
|
||||
│ └── test_cli.py # Unit tests
|
||||
├── setup.sh # Setup script for environment
|
||||
├── run.sh # Run script to execute the application
|
||||
├── requirements.txt # Project dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd reddit-cli
|
||||
```
|
||||
|
||||
2. Create and activate virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Run the CLI interface with a search query:
|
||||
```bash
|
||||
python -m src.main "python programming"
|
||||
```
|
||||
|
||||
Or run without arguments to enter interactive mode:
|
||||
```bash
|
||||
python -m src.main
|
||||
```
|
||||
|
||||
## Keyboard Controls
|
||||
|
||||
- `n` - Go to next page of results
|
||||
- `c` - Cycle through comments when viewing a post
|
||||
- `s` - Get AI-generated summary for current post
|
||||
- `q` or Ctrl+C - Quit the application
|
||||
- Number keys - Select specific post by number
|
||||
|
||||
## Configuration
|
||||
|
||||
The application uses environment variables for configuration:
|
||||
|
||||
- `OLLAMA_BASE_URL` - Ollama server address (default: http://192.168.8.223:11434)
|
||||
- `OLLAMA_MODEL` - AI model to use (default: gpt-oss:20b)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- requests
|
||||
- rich
|
||||
- pyyaml
|
||||
- pytest
|
||||
- pytest-cov
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
Or run basic functionality checks:
|
||||
```bash
|
||||
python test_cli.py
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
181
plan.md
Normal file
181
plan.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Reddit CLI Interface - Project Plan
|
||||
|
||||
## Overview
|
||||
Create a command-line interface for browsing Reddit posts with intuitive navigation, search capabilities, and interactive post viewing.
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Search Functionality
|
||||
- Search for Reddit posts by keyword/terms
|
||||
- Display first 15 results per page
|
||||
- Pagination support (press 'n' for next 15 posts)
|
||||
- Posts numbered for easy selection
|
||||
- Table format with:
|
||||
- Post number
|
||||
- Subreddit name
|
||||
- Post creation date
|
||||
- Post title (main column)
|
||||
|
||||
### 2. Post Navigation
|
||||
- Click on post number to view full post details
|
||||
- View main body text of the post
|
||||
- Interactive comments viewing (press 'c' to cycle through comments)
|
||||
- Summary functionality (press 's' to get AI-generated summary)
|
||||
|
||||
### 3. AI Integration
|
||||
- Use Ollama API at IP 192.168.8.223:11434
|
||||
- Model: gpt-oss:20b
|
||||
- Send post body + 100 random comments to AI for summarization
|
||||
- Display AI-generated summary to user
|
||||
|
||||
## Technical Implementation Plan
|
||||
|
||||
### Phase 1: Project Structure and Basic Setup
|
||||
- Create directory structure
|
||||
- Initialize project with dependencies
|
||||
- Set up basic CLI framework using a library like `clap` for Rust or `commander` for Node.js
|
||||
- Implement file-based data storage for caching Reddit data
|
||||
|
||||
### Phase 2: Search Functionality
|
||||
- Implement Reddit API integration (using official Reddit API or third-party)
|
||||
- Create search endpoint handler
|
||||
- Design result table format with required columns
|
||||
- Implement pagination logic ('n' key to load next page)
|
||||
|
||||
### Phase 3: Post Viewing Interface
|
||||
- Create post detail view
|
||||
- Display post body text
|
||||
- Implement interactive comment viewing system (press 'c' for comments)
|
||||
- Add UI for showing current post information
|
||||
|
||||
### Phase 4: AI Integration
|
||||
- Set up connection to Ollama server at 192.168.8.223:11434
|
||||
- Implement summary request functionality (press 's')
|
||||
- Process and display AI-generated summaries
|
||||
- Handle error scenarios for AI service unavailability
|
||||
|
||||
### Phase 5: User Experience Optimization
|
||||
- Add keyboard navigation hints to UI
|
||||
- Implement input validation
|
||||
- Create error handling for API failures
|
||||
- Add loading indicators for data fetching
|
||||
- Optimize performance for large comment sets
|
||||
|
||||
## Data Structure Requirements
|
||||
|
||||
### Post Representation
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"title": "string",
|
||||
"body": "string",
|
||||
"subreddit": "string",
|
||||
"created_utc": "timestamp",
|
||||
"url": "string",
|
||||
"comments": ["comment1", "comment2", ...]
|
||||
}
|
||||
```
|
||||
|
||||
### UI Components
|
||||
1. Search Interface
|
||||
2. Results Table View
|
||||
3. Post Detail View
|
||||
4. Comments Viewer
|
||||
5. AI Summary Display
|
||||
|
||||
## Detailed Technical Specifications
|
||||
|
||||
### API Integration Layer
|
||||
- Use Reddit's official API (https://www.reddit.com/dev/api/)
|
||||
- Authentication handling (OAuth or API key if needed)
|
||||
- Rate limiting implementation
|
||||
- Data caching to reduce API calls
|
||||
- Error handling for network issues and API errors
|
||||
|
||||
### CLI Interface Components
|
||||
- Main search screen with table view
|
||||
- Post detail screen showing:
|
||||
- Title
|
||||
- Author and subreddit info
|
||||
- Creation date
|
||||
- Body text
|
||||
- Comment viewer with cycling through posts
|
||||
- Summary screen showing AI output
|
||||
- Navigation controls with clear key hints
|
||||
|
||||
### Terminal UI Considerations
|
||||
- Use a library like `tui-rs` for Rust or `blessed` for Node.js for advanced terminal rendering
|
||||
- Support for color-coded UI elements (subreddit colors, etc.)
|
||||
- Responsiveness to different terminal sizes
|
||||
- Keyboard navigation support (arrows, enter, etc.)
|
||||
|
||||
### Data Management
|
||||
- Local cache storage using JSON files or a simple database
|
||||
- Cache invalidation strategy for fresh data
|
||||
- Efficient data parsing and storage formats
|
||||
|
||||
## Dependencies and Tools
|
||||
|
||||
### Core Libraries
|
||||
- CLI library (e.g., `clap` for Rust or `inquirer.js` for Node.js)
|
||||
- HTTP client for Reddit API communication (e.g., `reqwest` for Rust or `axios` for Node.js)
|
||||
- JSON parsing/serialization
|
||||
- Terminal UI framework (for enhanced display)
|
||||
|
||||
### AI Integration
|
||||
- Ollama client library for connecting to local AI server
|
||||
- Connection pooling and error handling for AI service
|
||||
- Request/response data formatting for the AI model
|
||||
|
||||
## Development Timeline
|
||||
|
||||
### Week 1
|
||||
- Project setup and core CLI framework
|
||||
- Basic Reddit API integration
|
||||
- Search functionality implementation
|
||||
- Initial terminal UI design
|
||||
|
||||
### Week 2
|
||||
- Results table formatting and pagination
|
||||
- Post detail view creation
|
||||
- Comments viewing system
|
||||
- Keyboard input handling
|
||||
|
||||
### Week 3
|
||||
- AI integration with Ollama server
|
||||
- Summary generation and display
|
||||
- Error handling for both API and AI service
|
||||
- Testing and UI refinement
|
||||
|
||||
### Week 4
|
||||
- Performance optimization
|
||||
- Error handling and edge case management
|
||||
- Documentation and final testing
|
||||
- User experience improvements
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Error Handling
|
||||
- Implement graceful degradation when services are unavailable
|
||||
- Network timeout handling
|
||||
- Data validation and sanitization
|
||||
- Clear error messages to user
|
||||
|
||||
### Usability Features
|
||||
- Clear visual feedback for user actions
|
||||
- Helpful key bindings documentation
|
||||
- Progress indicators for loading content
|
||||
- Responsive design for different terminal sizes
|
||||
|
||||
### Performance Optimization
|
||||
- Efficient data fetching strategies
|
||||
- Local caching of frequently accessed data
|
||||
- Asynchronous loading where appropriate
|
||||
- Memory management for large comment sets
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for core logic functions
|
||||
- Integration tests for API interactions
|
||||
- End-to-end tests for full user flows
|
||||
- UI component testing for terminal interaction
|
||||
- Performance testing with larger datasets
|
||||
13
prompt.md
Normal file
13
prompt.md
Normal file
@ -0,0 +1,13 @@
|
||||
let's making a planning doc called "plan.md" for the work we're going to do in this project. You're making this doc as your notes docs so that you can come back and implement all of it later.
|
||||
|
||||
I want to build an reddit cli interface so that I can navigate through some of the reddit posts on the cli. I want the interface to be intuitive and very easy to read.
|
||||
|
||||
The core feature is search. When I search, i want it to bring up all the posts that are under that search term. It can just bring up the first 15 results for the first page. Then if I type 'n' it should show the next 15 and so on.
|
||||
|
||||
Each post should be numbered. Also have a column in the table with the name of the subreddit and another column with the date the thread was created.
|
||||
|
||||
The user should be able to click one of the numbers of the post and then you should go into the post to view it.
|
||||
|
||||
For the view of a post, you should show the main body of text of the post. If they user would like a summary of the article, they can press 's' to get a summary, then you should send the body of the text and the 100 random comments to the gpt-oss:20b model running on a network server under ip 192.168.8.223:11434. Ask the ai to give the summary and then present the summary to the user.
|
||||
|
||||
When the user is viewing the post, they can click c to see the comments on the post. Each time they press c, let them see each line of comments.
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
# Reddit CLI Dependencies
|
||||
|
||||
# Core libraries
|
||||
requests>=2.20.0
|
||||
rich>=10.0.0
|
||||
pyyaml>=5.4.0
|
||||
|
||||
# Testing
|
||||
pytest>=6.0.0
|
||||
pytest-cov>=2.10.0
|
||||
19
run.sh
Executable file
19
run.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the Reddit CLI application
|
||||
# This script provides an easy way to run the Reddit CLI application
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Virtual environment not found. Please run setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Run the application
|
||||
python -m src.main "$@"
|
||||
|
||||
# Deactivate virtual environment
|
||||
deactivate
|
||||
38
setup.sh
Normal file
38
setup.sh
Normal file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Reddit CLI Setup Script
|
||||
# This script sets up the environment for the Reddit CLI application
|
||||
|
||||
echo "Setting up Reddit CLI Environment..."
|
||||
|
||||
# Check if Python 3 is installed
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Python 3 is not installed. Please install Python 3 to continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
# Upgrade pip
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install requirements
|
||||
echo "Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Setup complete!"
|
||||
echo ""
|
||||
echo "To run the application:"
|
||||
echo " source venv/bin/activate"
|
||||
echo " python -m src.main \"your search query\""
|
||||
echo ""
|
||||
echo "Or in interactive mode:"
|
||||
echo " source venv/bin/activate"
|
||||
echo " python -m src.main"
|
||||
102
src/README.md
Normal file
102
src/README.md
Normal file
@ -0,0 +1,102 @@
|
||||
# Reddit CLI
|
||||
|
||||
A command-line interface for browsing Reddit posts with search capabilities, interactive post viewing, and AI-powered summarization.
|
||||
|
||||
## Features
|
||||
|
||||
- **Search Functionality**: Search for Reddit posts by keyword/terms
|
||||
- **Results Display**: Table view showing:
|
||||
- Post number
|
||||
- Subreddit name
|
||||
- Post creation date
|
||||
- Post title (main column)
|
||||
- **Post Navigation**: Click on post number to view full post details
|
||||
- **Comments Viewer**: Cycle through comments using 'c' key
|
||||
- **AI Integration**: Get AI-generated summaries using Ollama API
|
||||
- **Pagination**: Navigate between pages of results ('n' key)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd reddit-cli
|
||||
```
|
||||
|
||||
2. Create and activate virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Run the CLI interface with a search query:
|
||||
```bash
|
||||
python main.py "python programming"
|
||||
```
|
||||
|
||||
Or run without arguments to enter interactive mode:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Keyboard Controls
|
||||
|
||||
- `n` - Go to next page of results
|
||||
- `c` - Cycle through comments when viewing a post
|
||||
- `s` - Get AI-generated summary for current post
|
||||
- `q` or Ctrl+C - Quit the application
|
||||
- Number keys - Select specific post by number
|
||||
|
||||
## Configuration
|
||||
|
||||
The application uses environment variables for configuration:
|
||||
|
||||
- `OLLAMA_BASE_URL` - Ollama server address (default: http://192.168.8.223:11434)
|
||||
- `OLLAMA_MODEL` - AI model to use (default: gpt-oss:20b)
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `main.py` - Main application entrypoint
|
||||
- `reddit_client.py` - Reddit API client implementation
|
||||
- `ai_client.py` - Ollama AI integration
|
||||
- `test_cli.py` - Unit tests
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- requests
|
||||
- rich
|
||||
- pyyaml
|
||||
- pytest
|
||||
- pytest-cov
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
Or run basic functionality checks:
|
||||
```bash
|
||||
python test_cli.py
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
5
src/__init__.py
Normal file
5
src/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
Reddit CLI Package
|
||||
|
||||
This package contains the core functionality for the Reddit CLI application.
|
||||
"""
|
||||
612
src/main.py
Normal file
612
src/main.py
Normal file
@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reddit CLI Interface - Python Implementation
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
|
||||
class RedditClient:
|
||||
"""Client for interacting with Reddit API"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = "https://www.reddit.com"
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"User-Agent": "RedditCLI/0.1 by User"})
|
||||
|
||||
def search_posts(
|
||||
self, query: str, limit: int = 15, after: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Search for Reddit posts matching the query
|
||||
|
||||
Args:
|
||||
query: Search terms
|
||||
limit: Number of posts to return (default 15)
|
||||
after: Pagination token for next page
|
||||
|
||||
Returns:
|
||||
Dictionary containing search results and pagination info
|
||||
"""
|
||||
params = {"q": query, "limit": limit, "sort": "hot", "type": "link"}
|
||||
|
||||
if after:
|
||||
params["after"] = after
|
||||
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/search.json", params=params, timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Failed to fetch posts: {str(e)}")
|
||||
|
||||
def get_post_details(self, post_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get detailed information about a specific post
|
||||
|
||||
Args:
|
||||
post_id: Reddit post ID
|
||||
|
||||
Returns:
|
||||
Dictionary with post details
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/by_id/t3_{post_id}.json", timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Extract post from the response structure
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
return data[0].get("data", {})
|
||||
elif isinstance(data, dict):
|
||||
return data.get("data", {})
|
||||
|
||||
return {}
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Failed to fetch post details: {str(e)}")
|
||||
|
||||
def get_post_comments(self, post_id: str, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get comments for a specific post
|
||||
|
||||
Args:
|
||||
post_id: Reddit post ID
|
||||
limit: Maximum number of comments to fetch
|
||||
|
||||
Returns:
|
||||
List of comment dictionaries
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/comments/{post_id}.json",
|
||||
params={"limit": limit},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Extract comments from the nested structure
|
||||
comments = []
|
||||
if isinstance(data, list) and len(data) > 1:
|
||||
comment_data = data[1].get("data", {}).get("children", [])
|
||||
for child in comment_data:
|
||||
comment = child.get("data", {})
|
||||
# Flatten the comment structure to include author and body
|
||||
comments.append(
|
||||
{
|
||||
"author": comment.get("author", "unknown"),
|
||||
"body": comment.get("body", ""),
|
||||
"score": comment.get("score", 0),
|
||||
"created_utc": comment.get("created_utc", 0),
|
||||
}
|
||||
)
|
||||
|
||||
return comments
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Failed to fetch comments: {str(e)}")
|
||||
|
||||
def format_timestamp(self, timestamp: int) -> str:
|
||||
"""
|
||||
Format Unix timestamp into readable date string
|
||||
|
||||
Args:
|
||||
timestamp: Unix timestamp
|
||||
|
||||
Returns:
|
||||
Formatted date string
|
||||
"""
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
class AIClient:
|
||||
"""Client for interacting with Ollama AI API"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = os.getenv("OLLAMA_BASE_URL", "http://192.168.8.223:11434")
|
||||
self.model = os.getenv("OLLAMA_MODEL", "gpt-oss:20b")
|
||||
self.session = requests.Session()
|
||||
|
||||
def generate_summary(self, post_body: str, comments: List[str]) -> str:
|
||||
"""
|
||||
Generate AI summary of a post with comments
|
||||
|
||||
Args:
|
||||
post_body: The main body text of the post
|
||||
comments: List of comment strings
|
||||
|
||||
Returns:
|
||||
Generated summary from AI
|
||||
"""
|
||||
# Select 100 random comments (or all if less than 100)
|
||||
selected_comments = comments[:100]
|
||||
|
||||
# Format prompt for the AI model
|
||||
prompt = self._create_prompt(post_body, selected_comments)
|
||||
|
||||
try:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/generate",
|
||||
json={"model": self.model, "prompt": prompt, "stream": False},
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
return data.get("response", "").strip()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Failed to generate AI summary: {str(e)}")
|
||||
|
||||
def _create_prompt(self, post_body: str, comments: List[str]) -> str:
|
||||
"""
|
||||
Create a formatted prompt for the AI with post and comments
|
||||
|
||||
Args:
|
||||
post_body: The main body text of the post
|
||||
comments: List of comment strings
|
||||
|
||||
Returns:
|
||||
Formatted prompt string
|
||||
"""
|
||||
# Join comments into a single string with proper formatting
|
||||
comments_text = "\n".join(
|
||||
[f"Comment {i + 1}: {comment}" for i, comment in enumerate(comments)]
|
||||
)
|
||||
|
||||
if not comments_text:
|
||||
comments_text = "No comments available."
|
||||
|
||||
prompt = f"""
|
||||
Summarize the following Reddit post and its comments in 2-3 sentences.
|
||||
|
||||
Post:
|
||||
{post_body}
|
||||
|
||||
Comments:
|
||||
{comments_text}
|
||||
|
||||
Summary:
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class RedditCLI:
|
||||
def __init__(self):
|
||||
self.console = Console()
|
||||
self.reddit_client = RedditClient()
|
||||
self.ai_client = AIClient()
|
||||
self.current_page = 0
|
||||
self.search_results = []
|
||||
self.current_post = None
|
||||
self.comments = []
|
||||
self.current_comment_index = 0
|
||||
self.search_query = ""
|
||||
self.after_token = None
|
||||
|
||||
def run(self):
|
||||
"""Main application entry point"""
|
||||
parser = argparse.ArgumentParser(description="Reddit CLI Interface")
|
||||
parser.add_argument("query", nargs="?", help="Search query for Reddit")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.query:
|
||||
# Show welcome screen
|
||||
self.show_welcome()
|
||||
# Get search query from user
|
||||
query = input("\nEnter search query: ").strip()
|
||||
if not query:
|
||||
self.console.print("[red]No query provided. Exiting.[/red]")
|
||||
return
|
||||
self.search(query)
|
||||
else:
|
||||
# Process the provided query
|
||||
self.search(args.query)
|
||||
|
||||
def show_welcome(self):
|
||||
"""Display welcome screen"""
|
||||
self.console.print(
|
||||
Panel(
|
||||
"Reddit CLI Interface\n\n"
|
||||
"Use 'n' to go to next page\n"
|
||||
"Press number to view post details\n"
|
||||
"Press 'c' to cycle comments\n"
|
||||
"Press 's' to get AI summary\n"
|
||||
"Press 'S' (capital S) for new search\n"
|
||||
"Press 'q' or Ctrl+C to quit",
|
||||
title="Welcome to Reddit CLI",
|
||||
border_style="blue",
|
||||
)
|
||||
)
|
||||
|
||||
def search(self, query: str):
|
||||
"""Perform search and display results"""
|
||||
self.search_query = query
|
||||
self.console.print(f"\nSearching for: [bold blue]{query}[/bold blue]")
|
||||
|
||||
# Retry logic for initial search
|
||||
max_retries = 2
|
||||
retry_delay = 1 # seconds
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
# Reset pagination state
|
||||
self.after_token = None
|
||||
|
||||
data = self.reddit_client.search_posts(query, limit=15)
|
||||
|
||||
# Extract posts from response
|
||||
posts = []
|
||||
|
||||
# Check if we have data in the expected response format
|
||||
if "data" in data and "children" in data["data"]:
|
||||
for child in data["data"]["children"]:
|
||||
post_data = child.get("data", {})
|
||||
if post_data:
|
||||
posts.append(
|
||||
{
|
||||
"id": post_data.get("id"),
|
||||
"title": post_data.get("title", "No title"),
|
||||
"subreddit": post_data.get("subreddit", "unknown"),
|
||||
"created_utc": post_data.get("created_utc", 0),
|
||||
"url": post_data.get("url", ""),
|
||||
"body": post_data.get(
|
||||
"selftext", post_data.get("body", "")
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Get the after token for pagination
|
||||
self.after_token = data["data"].get("after")
|
||||
|
||||
if not posts:
|
||||
self.console.print("[yellow]No results found[/yellow]")
|
||||
return
|
||||
|
||||
self.search_results = posts
|
||||
self.current_page = 0
|
||||
self.display_search_results()
|
||||
self.handle_user_input()
|
||||
return # Success, exit retry loop
|
||||
|
||||
except Exception as e:
|
||||
if attempt < max_retries:
|
||||
self.console.print(
|
||||
f"[yellow]Attempt {attempt + 1} failed: {e}[/yellow]"
|
||||
)
|
||||
self.console.print(
|
||||
f"[yellow]Retrying in {retry_delay} seconds...[/yellow]"
|
||||
)
|
||||
time.sleep(retry_delay)
|
||||
retry_delay *= 2 # Exponential backoff
|
||||
else:
|
||||
self.console.print(
|
||||
f"[red]Error searching after {max_retries + 1} attempts: {e}[/red]"
|
||||
)
|
||||
self.console.print("[yellow]Please try a new search.[/yellow]")
|
||||
# Re-raise exception so it can be handled by the main loop
|
||||
raise e
|
||||
|
||||
def next_page(self, query: str):
|
||||
"""Load the next page of search results"""
|
||||
if not self.after_token:
|
||||
self.console.print("[yellow]No more pages available[/yellow]")
|
||||
return
|
||||
|
||||
# Retry logic for network errors
|
||||
max_retries = 2
|
||||
retry_delay = 1 # seconds
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
data = self.reddit_client.search_posts(
|
||||
query, limit=15, after=self.after_token
|
||||
)
|
||||
|
||||
# Extract posts from response
|
||||
posts = []
|
||||
|
||||
# Check if we have data in the expected response format
|
||||
if "data" in data and "children" in data["data"]:
|
||||
for child in data["data"]["children"]:
|
||||
post_data = child.get("data", {})
|
||||
if post_data:
|
||||
posts.append(
|
||||
{
|
||||
"id": post_data.get("id"),
|
||||
"title": post_data.get("title", "No title"),
|
||||
"subreddit": post_data.get("subreddit", "unknown"),
|
||||
"created_utc": post_data.get("created_utc", 0),
|
||||
"url": post_data.get("url", ""),
|
||||
"body": post_data.get(
|
||||
"selftext", post_data.get("body", "")
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Get the after token for pagination
|
||||
self.after_token = data["data"].get("after")
|
||||
|
||||
if not posts:
|
||||
self.console.print("[yellow]No more results found[/yellow]")
|
||||
return
|
||||
|
||||
self.search_results = posts
|
||||
self.current_page += 1
|
||||
|
||||
self.display_search_results()
|
||||
self.handle_user_input()
|
||||
return # Success, exit retry loop
|
||||
|
||||
except Exception as e:
|
||||
if attempt < max_retries:
|
||||
self.console.print(
|
||||
f"[yellow]Attempt {attempt + 1} failed: {e}[/yellow]"
|
||||
)
|
||||
self.console.print(
|
||||
f"[yellow]Retrying in {retry_delay} seconds...[/yellow]"
|
||||
)
|
||||
time.sleep(retry_delay)
|
||||
retry_delay *= 2 # Exponential backoff
|
||||
else:
|
||||
self.console.print(
|
||||
f"[red]Error loading next page after {max_retries + 1} attempts: {e}[/red]"
|
||||
)
|
||||
self.console.print(
|
||||
"[yellow]Returning to current results. Try a new search.[/yellow]"
|
||||
)
|
||||
self.display_search_results()
|
||||
# Re-raise exception so it can be handled by the main loop
|
||||
raise e
|
||||
|
||||
def display_search_results(self):
|
||||
"""Display search results in a table format"""
|
||||
table = Table(title=f"Search Results (Page {self.current_page + 1})")
|
||||
table.add_column("Number", style="cyan", no_wrap=True)
|
||||
table.add_column("Subreddit", style="magenta")
|
||||
table.add_column("Date", style="green")
|
||||
table.add_column("Title", style="white")
|
||||
|
||||
for i, post in enumerate(self.search_results):
|
||||
# Format date if available
|
||||
formatted_date = self.reddit_client.format_timestamp(post["created_utc"])
|
||||
|
||||
table.add_row(str(i + 1), post["subreddit"], formatted_date, post["title"])
|
||||
|
||||
self.console.print(table)
|
||||
self.console.print(
|
||||
"\n[blue]Press 'n' for next page, number to select a post, or 'q' to quit[/blue]"
|
||||
)
|
||||
|
||||
def handle_user_input(self):
|
||||
"""Handle user interaction and navigation"""
|
||||
try:
|
||||
while True:
|
||||
user_input = input("\nEnter selection: ").strip().lower()
|
||||
|
||||
if user_input == "n":
|
||||
# Move to next page
|
||||
self.console.print("[yellow]Loading next page...[/yellow]")
|
||||
try:
|
||||
self.next_page(self.search_query)
|
||||
break # Break out of current loop so we can show new results
|
||||
except Exception as e:
|
||||
# If all retries failed, let user start a new search
|
||||
pass # Continue to main loop to allow new search
|
||||
|
||||
elif user_input == "s":
|
||||
# Perform new search
|
||||
self.console.print("[blue]Starting new search...[/blue]")
|
||||
query = input("Enter new search query: ").strip()
|
||||
if query:
|
||||
self.search(query)
|
||||
break
|
||||
else:
|
||||
self.console.print(
|
||||
"[yellow]No query provided. Returning to current results.[/yellow]"
|
||||
)
|
||||
self.display_search_results()
|
||||
|
||||
elif user_input in ["q", "quit"]:
|
||||
self.console.print("Goodbye!")
|
||||
break
|
||||
|
||||
elif user_input.isdigit():
|
||||
index = int(user_input) - 1
|
||||
if 0 <= index < len(self.search_results):
|
||||
self.view_post(index)
|
||||
else:
|
||||
self.console.print("[red]Invalid selection[/red]")
|
||||
|
||||
else:
|
||||
self.console.print(
|
||||
"[red]Unknown command. Use 'n' for next page, 's' for new search, number to select post, or 'q' to quit[/red]"
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.console.print("\n[cyan]Goodbye![/cyan]")
|
||||
|
||||
def view_post(self, index: int):
|
||||
"""View a selected post with full details"""
|
||||
self.current_post = self.search_results[index]
|
||||
|
||||
# Try to get more detailed info
|
||||
try:
|
||||
detailed_info = self.reddit_client.get_post_details(self.current_post["id"])
|
||||
if detailed_info:
|
||||
self.current_post.update(detailed_info)
|
||||
|
||||
# Get comments
|
||||
self.comments = self.reddit_client.get_post_comments(
|
||||
self.current_post["id"], limit=100
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Error fetching details: {e}[/red]")
|
||||
|
||||
# Display post details
|
||||
self.display_post_details()
|
||||
|
||||
# Handle post-specific navigation
|
||||
try:
|
||||
while True:
|
||||
user_input = (
|
||||
input(
|
||||
"\n[blue]Enter 'c' to view comments, 's' for AI summary, 'S' for new search, or any other key to return to search: [/blue]"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
|
||||
if user_input == "c":
|
||||
self.view_comments()
|
||||
elif user_input == "s":
|
||||
self.view_ai_summary()
|
||||
elif user_input == "S": # Capital S for new search from post view
|
||||
self.console.print("[blue]Starting new search...[/blue]")
|
||||
query = input("Enter new search query: ").strip()
|
||||
if query:
|
||||
self.search(query)
|
||||
break
|
||||
else:
|
||||
self.console.print(
|
||||
"[yellow]No query provided. Returning to current post.[/yellow]"
|
||||
)
|
||||
self.display_post_details()
|
||||
else:
|
||||
# Return to search results
|
||||
self.display_search_results()
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.console.print("\n[blue]Returning to search...[/blue]")
|
||||
|
||||
def display_post_details(self):
|
||||
"""Display full details of a post"""
|
||||
self.console.print(
|
||||
Panel(
|
||||
f"[bold blue]{self.current_post.get('title', 'No title')}[/bold blue]\n\n"
|
||||
f"Subreddit: [cyan]{self.current_post.get('subreddit', 'unknown')}[/cyan]\n"
|
||||
f"Created: {self.reddit_client.format_timestamp(self.current_post.get('created_utc', 0))}\n\n"
|
||||
f"[white]{self.current_post.get('body', '')}[/white]\n",
|
||||
title=f"Post #{self.search_results.index(self.current_post) + 1}",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
|
||||
def view_comments(self):
|
||||
"""View comments with cycling"""
|
||||
if not self.comments:
|
||||
self.console.print("[yellow]No comments available[/yellow]")
|
||||
return
|
||||
|
||||
self.console.print(f"[blue]Showing comments: {len(self.comments)} total[/blue]")
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Display current comment
|
||||
comment = self.comments[self.current_comment_index]
|
||||
self.console.print(
|
||||
Panel(
|
||||
f"Author: [cyan]{comment.get('author', 'unknown')}[/cyan]\n"
|
||||
f"Score: [magenta]{comment.get('score', 0)}[/magenta]\n\n"
|
||||
f"[white]{comment.get('body', '')}[/white]",
|
||||
title=f"Comment {self.current_comment_index + 1}",
|
||||
border_style="yellow",
|
||||
)
|
||||
)
|
||||
|
||||
# Navigation options
|
||||
user_input = (
|
||||
input(
|
||||
"\n[blue]Press 'n' for next comment, 'p' for previous, or any key to return: [/blue]"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
|
||||
if user_input == "n":
|
||||
self.current_comment_index = (self.current_comment_index + 1) % len(
|
||||
self.comments
|
||||
)
|
||||
elif user_input == "p":
|
||||
self.current_comment_index = (self.current_comment_index - 1) % len(
|
||||
self.comments
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.console.print("\n[blue]Returning to post...[/blue]")
|
||||
|
||||
def view_ai_summary(self):
|
||||
"""Get and display AI-generated summary via Ollama"""
|
||||
if not self.current_post or not self.comments:
|
||||
self.console.print("[red]No content available for summarization[/red]")
|
||||
return
|
||||
|
||||
try:
|
||||
body = self.current_post.get("body", "")
|
||||
|
||||
# Get the first 100 comments (or fewer)
|
||||
comment_bodies = [comment.get("body", "") for comment in self.comments]
|
||||
|
||||
summary = self.ai_client.generate_summary(body, comment_bodies)
|
||||
|
||||
if summary:
|
||||
self.console.print(
|
||||
Panel(
|
||||
f"[bold green]AI Summary:[/bold green]\n\n{summary}",
|
||||
title="AI Generated Summary",
|
||||
border_style="magenta",
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.console.print("[red]Failed to generate AI summary[/red]")
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Error generating AI summary: {e}[/red]")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the CLI application"""
|
||||
app = RedditCLI()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
48
src/pagination.md
Normal file
48
src/pagination.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Reddit CLI Pagination Implementation
|
||||
|
||||
## Overview
|
||||
This document describes the pagination implementation for the Reddit CLI application, enabling users to navigate through multiple pages of search results.
|
||||
|
||||
## Current Implementation Status
|
||||
The basic search functionality is implemented but pagination is not yet fully functional. This document outlines how to properly implement pagination support.
|
||||
|
||||
## Key Requirements
|
||||
1. Users can press 'n' to load next page of results
|
||||
2. Handle pagination tokens from Reddit API responses
|
||||
3. Maintain state between pages
|
||||
4. Display current page information in UI
|
||||
5. Continue fetching results until no more pages exist
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Reddit API Pagination
|
||||
The Reddit API uses the `after` parameter for pagination:
|
||||
- First request: No `after` parameter
|
||||
- Subsequent requests: Use `after` parameter with token from previous response
|
||||
- Response includes `after` field for next page
|
||||
|
||||
### Implementation Approach
|
||||
1. Modify search method to accept pagination token
|
||||
2. Extract 'after' token from API responses
|
||||
3. Implement page tracking in the application state
|
||||
4. Update display to show current page number
|
||||
5. Handle end of results gracefully
|
||||
|
||||
## File Modifications Needed
|
||||
- main.py (add pagination logic)
|
||||
- reddit_client.py (update search_posts method for pagination)
|
||||
|
||||
## Example Flow
|
||||
1. User searches "python"
|
||||
2. First page loads with 15 results
|
||||
3. User presses 'n'
|
||||
4. Application fetches next page using 'after' token from previous response
|
||||
5. Repeat until no more pages
|
||||
|
||||
## Next Steps
|
||||
1. Update search_posts in reddit_client.py to handle pagination tokens
|
||||
2. Implement next page logic in main.py
|
||||
3. Add state tracking for current page
|
||||
4. Update UI to display page information
|
||||
5. Handle edge cases and errors gracefully
|
||||
```
|
||||
61
tests/test_cli.py
Normal file
61
tests/test_cli.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reddit CLI - Unit Tests
|
||||
|
||||
This module contains unit tests for the Reddit CLI application components.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Add the project directory to Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from ai_client import AIClient
|
||||
from reddit_client import RedditClient
|
||||
|
||||
|
||||
class TestRedditClient(unittest.TestCase):
|
||||
"""Test cases for RedditClient class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.client = RedditClient()
|
||||
|
||||
def test_client_initialization(self):
|
||||
"""Test that RedditClient initializes correctly"""
|
||||
self.assertIsNotNone(self.client)
|
||||
self.assertEqual(self.client.base_url, "https://www.reddit.com")
|
||||
|
||||
def test_prompt_creation(self):
|
||||
"""Test prompt creation functionality"""
|
||||
client = AIClient()
|
||||
|
||||
# Test with sample data
|
||||
post_body = "This is a test post body"
|
||||
comments = ["First comment", "Second comment"]
|
||||
|
||||
prompt = client._create_prompt(post_body, comments)
|
||||
|
||||
self.assertIn("Summarize", prompt)
|
||||
self.assertIn(post_body, prompt)
|
||||
self.assertIn("Comment 1:", prompt)
|
||||
|
||||
|
||||
class TestAIClient(unittest.TestCase):
|
||||
"""Test cases for AIClient class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.client = AIClient()
|
||||
|
||||
def test_client_initialization(self):
|
||||
"""Test that AIClient initializes correctly"""
|
||||
self.assertIsNotNone(self.client)
|
||||
self.assertEqual(self.client.model, "gpt-oss:20b")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the tests
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
x
Reference in New Issue
Block a user