Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
|
7316839d06 | |
|
6e18d412a9 | |
|
cd2c1bc285 | |
|
cc72d58994 | |
|
85bc4265cf | |
|
5f4e811e6f | |
|
a432cddf79 | |
|
bab5cdaf2e | |
|
e083420c5c | |
|
fa9a151d85 | |
|
8add7ff0ff | |
|
39f9e217b2 | |
|
ffa2bc9bd2 | |
|
c49bbd2692 | |
|
f0d3ef36e2 | |
|
146ea25974 | |
|
1d9a05fb51 | |
|
1baf800e3d | |
|
04ea44dcb7 |
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ clap = { version = "4.5.4", features = ["derive"] }
|
||||||
femme = "2.2.1"
|
femme = "2.2.1"
|
||||||
home = "0.5.9"
|
home = "0.5.9"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
openssl = { version = "0.10.68", features = ["vendored"] }
|
||||||
reqwest = "0.12.3"
|
reqwest = "0.12.3"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.115"
|
||||||
|
|
|
@ -45,7 +45,7 @@ you can set their paths and filenames via `HEY_CONFIG_PATH`, `HEY_CONFIG_FILENAM
|
||||||
|
|
||||||
## config file reference
|
## config file reference
|
||||||
```toml
|
```toml
|
||||||
model = "Claude" # or "GPT3"
|
model = "Claude" # or "GPT4OMini"
|
||||||
tos = false # whether if you agree to ddg chat tos
|
tos = false # whether if you agree to ddg chat tos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717067539,
|
||||||
|
"narHash": "sha256-oIs5EF+6VpHJRvvpVWuqCYJMMVW/6h59aYUv9lABLtY=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "fa19d8c135e776dc97f4dcca08656a0eeb28d5c0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 0,
|
||||||
|
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
|
||||||
|
"path": "/nix/store/8s55w0927lh3mdbkxf434zb0c5hqsz8z-source",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717646450,
|
||||||
|
"narHash": "sha256-KE+UmfSVk5PG8jdKdclPVcMrUB8yVZHbsjo7ZT1Bm3c=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "818dbe2f96df233d2041739d6079bb616d3e5597",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
naersk.url = "github:nix-community/naersk";
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, flake-utils, naersk, nixpkgs }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = (import nixpkgs) {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
naersk' = pkgs.callPackage naersk { };
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
defaultPackage = naersk'.buildPackage
|
||||||
|
{
|
||||||
|
src = ./.;
|
||||||
|
buildInputs = with pkgs; [ openssl pkg-config ];
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
openssl
|
||||||
|
pkg-config
|
||||||
|
clippy
|
||||||
|
rust-analyzer
|
||||||
|
];
|
||||||
|
nativeBuildInputs = with pkgs; [ rustc cargo ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
59
src/api.rs
59
src/api.rs
|
@ -5,23 +5,38 @@ use std::process::exit;
|
||||||
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
|
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{cache::Cache, config::Config};
|
use crate::{config::Config, history::HistoryObject, session::Session};
|
||||||
use crate::{WARN, RED, RESET};
|
use crate::{WARN, RED, RESET};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ChatMessagePayload {
|
pub struct ChatMessagePayload {
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ChatMessagePayload {
|
||||||
|
pub fn empty(role: String) -> Self {
|
||||||
|
Self {
|
||||||
|
role,
|
||||||
|
content: String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ChatPayload {
|
pub struct ChatPayload {
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub messages: Vec<ChatMessagePayload>
|
pub messages: Vec<ChatMessagePayload>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ChatMessagePayload {
|
||||||
|
pub fn is_ai(&self) -> bool {
|
||||||
|
self.role == "assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ChatChunk {
|
pub struct ChatChunk {
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub created: u64,
|
pub created: u64,
|
||||||
|
@ -30,8 +45,17 @@ struct ChatChunk {
|
||||||
pub model: Option<String>
|
pub model: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<ChatMessagePayload> for ChatChunk {
|
||||||
|
fn into(self) -> ChatMessagePayload {
|
||||||
|
ChatMessagePayload {
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ErrChatChunk {
|
pub struct ErrChatChunk {
|
||||||
pub action: String,
|
pub action: String,
|
||||||
pub status: u64,
|
pub status: u64,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
@ -87,13 +111,20 @@ pub async fn get_vqd(cli: &Client) -> Result<String, Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_res<'a>(cli: &Client, query: String, vqd: String, cache: &'a mut Cache, config: &Config) {
|
pub async fn get_res<'a>(cli: &Client, query: String, vqd: String, config: &Config) {
|
||||||
|
let init_msg = ChatMessagePayload { role: "user".into(), content: query };
|
||||||
|
|
||||||
|
let mut session = Session::create_or_restore(&vqd);
|
||||||
|
session.push_message(&init_msg).unwrap();
|
||||||
|
|
||||||
let payload = ChatPayload {
|
let payload = ChatPayload {
|
||||||
model: config.model.to_string(),
|
model: config.model.to_string(),
|
||||||
messages: vec![ ChatMessagePayload { role: "user".into(), content: query } ]
|
messages: session.get_messages()
|
||||||
};
|
};
|
||||||
let payload = serde_json::to_string(&payload).unwrap();
|
let payload = serde_json::to_string(&payload).unwrap();
|
||||||
|
|
||||||
|
println!("{}", init_msg.display_as_history());
|
||||||
|
|
||||||
let req = cli.post("https://duckduckgo.com/duckchat/v1/chat")
|
let req = cli.post("https://duckduckgo.com/duckchat/v1/chat")
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0")
|
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0")
|
||||||
|
@ -105,15 +136,17 @@ pub async fn get_res<'a>(cli: &Client, query: String, vqd: String, cache: &'a mu
|
||||||
let new_vqd = res.headers().iter().find(|x| x.0 == "x-vqd-4");
|
let new_vqd = res.headers().iter().find(|x| x.0 == "x-vqd-4");
|
||||||
let vqd_set_res =
|
let vqd_set_res =
|
||||||
if let Some(new_vqd) = new_vqd {
|
if let Some(new_vqd) = new_vqd {
|
||||||
cache.set_last_vqd(new_vqd.1.as_bytes().iter().map(|x| char::from(*x)).collect::<String>())
|
session.set_last_vqd(new_vqd.1.as_bytes().iter().map(|x| char::from(*x)).collect::<String>())
|
||||||
} else {
|
} else {
|
||||||
eprintln!("{WARN}Warn: DuckDuckGo did not return new VQD. Ignore this if everything else is ok.{RESET}");
|
eprintln!("{WARN}Warn: DuckDuckGo did not return new VQD. Ignore this if everything else is ok.{RESET}");
|
||||||
cache.set_last_vqd(vqd.clone())
|
session.set_last_vqd(vqd.clone())
|
||||||
};
|
};
|
||||||
if let Err(err) = vqd_set_res {
|
if let Err(err) = vqd_set_res {
|
||||||
eprintln!("{WARN}Warn: Could not save VQD to cache: {err}{RESET}");
|
eprintln!("{WARN}Warn: Could not save VQD to cache: {err}{RESET}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut error = None;
|
||||||
|
println!("{}", ChatMessagePayload::empty("assistant".into()).display_as_history());
|
||||||
while let Some(chunk) = res.chunk().await.unwrap() {
|
while let Some(chunk) = res.chunk().await.unwrap() {
|
||||||
|
|
||||||
if let Ok(obj) = serde_json::from_slice::<ErrChatChunk>(&chunk) {
|
if let Ok(obj) = serde_json::from_slice::<ErrChatChunk>(&chunk) {
|
||||||
|
@ -127,9 +160,17 @@ pub async fn get_res<'a>(cli: &Client, query: String, vqd: String, cache: &'a mu
|
||||||
let chunk = chunk.replace("data: ", "");
|
let chunk = chunk.replace("data: ", "");
|
||||||
for line in chunk.lines() {
|
for line in chunk.lines() {
|
||||||
if let Ok(obj) = serde_json::from_str::<ChatChunk>(line) {
|
if let Ok(obj) = serde_json::from_str::<ChatChunk>(line) {
|
||||||
|
if let Err(err) = session.push_ai_message_chunk(&obj) {
|
||||||
|
error = Some(err);
|
||||||
|
}
|
||||||
print!("{}", obj.message);
|
print!("{}", obj.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("\n");
|
println!("\n");
|
||||||
|
|
||||||
|
if let Some(err) = error {
|
||||||
|
eprintln!("Error while writing to session: {err:#?}");
|
||||||
|
eprintln!("Session may be broken.");
|
||||||
|
}
|
||||||
}
|
}
|
85
src/cache.rs
85
src/cache.rs
|
@ -1,85 +0,0 @@
|
||||||
|
|
||||||
use std::{env, error::Error, fs, io, path::PathBuf};
|
|
||||||
use home::home_dir;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Cache {
|
|
||||||
pub last_vqd: String,
|
|
||||||
pub last_vqd_time: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Cache {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
last_vqd: "".into(),
|
|
||||||
last_vqd_time: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cache {
|
|
||||||
|
|
||||||
pub fn get_path<T: From<String>>() -> T {
|
|
||||||
match env::var("HEY_CACHE_PATH") {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) =>
|
|
||||||
match home_dir() {
|
|
||||||
Some(home) => home.join(".cache/hey").as_os_str().as_encoded_bytes().iter().map(|x| char::from(*x)).collect::<String>(),
|
|
||||||
None => panic!("Cannot detect your home directory!")
|
|
||||||
}
|
|
||||||
}.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_file_name<T: From<String>>() -> T {
|
|
||||||
match env::var("HEY_CACHE_FILENAME") {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => "cache.json".into()
|
|
||||||
}.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_dir_exists() -> io::Result<()> {
|
|
||||||
let path: PathBuf = Self::get_path();
|
|
||||||
if ! path.is_dir() { fs::create_dir_all(path)? }
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load() -> Result<Self, Box<dyn Error>> {
|
|
||||||
let path: PathBuf = Self::get_path();
|
|
||||||
Self::ensure_dir_exists()?;
|
|
||||||
|
|
||||||
let file_path = path.join(Self::get_file_name::<PathBuf>());
|
|
||||||
if ! file_path.is_file() {
|
|
||||||
let def = Self::default();
|
|
||||||
def.save()?;
|
|
||||||
Ok(def)
|
|
||||||
} else {
|
|
||||||
let file = fs::read_to_string(file_path)?;
|
|
||||||
Ok(serde_json::from_str(&file)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(self: &Self) -> Result<(), Box<dyn Error>> {
|
|
||||||
let path: PathBuf = Self::get_path();
|
|
||||||
Self::ensure_dir_exists()?;
|
|
||||||
|
|
||||||
let file_path = path.join(Self::get_file_name::<PathBuf>());
|
|
||||||
fs::write(file_path, serde_json::to_string_pretty(self)?)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_last_vqd<T: Into<String>>(self: &mut Self, vqd: T) -> Result<(), Box<dyn Error>> {
|
|
||||||
self.last_vqd = vqd.into();
|
|
||||||
self.last_vqd_time = chrono::Local::now().timestamp_millis() as u64;
|
|
||||||
self.save()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_vqd<'a, T: From<&'a String>>(self: &'a Self) -> Option<T> {
|
|
||||||
if self.last_vqd_time - (chrono::Local::now().timestamp_millis() as u64) < 60000 {
|
|
||||||
Some((&self.last_vqd).into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::{env, error::Error, fs, io, path::PathBuf};
|
use crate::{RED, BLUE, WARN, RESET, GRAY};
|
||||||
|
|
||||||
|
use std::{env, error::Error, fs, io, path::PathBuf, process::exit};
|
||||||
|
|
||||||
use home::home_dir;
|
use home::home_dir;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -8,10 +10,11 @@ pub enum Model {
|
||||||
// outdated
|
// outdated
|
||||||
Claude12,
|
Claude12,
|
||||||
GPT35,
|
GPT35,
|
||||||
|
GPT3,
|
||||||
|
|
||||||
// current
|
// current
|
||||||
Claude,
|
Claude,
|
||||||
GPT3,
|
GPT4OMini,
|
||||||
Llama,
|
Llama,
|
||||||
Mixtral
|
Mixtral
|
||||||
}
|
}
|
||||||
|
@ -24,8 +27,9 @@ impl ToString for Model {
|
||||||
|
|
||||||
Self::Claude => String::from("claude-3-haiku-20240307"),
|
Self::Claude => String::from("claude-3-haiku-20240307"),
|
||||||
Self::GPT3 => String::from("gpt-3.5-turbo-0125"),
|
Self::GPT3 => String::from("gpt-3.5-turbo-0125"),
|
||||||
Self::Llama => String::from("meta-llama/Llama-3-70b-chat-hf"),
|
Self::Llama => String::from("meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"),
|
||||||
Self::Mixtral => String::from("mistralai/Mixtral-8x7B-Instruct-v0.1")
|
Self::Mixtral => String::from("mistralai/Mixtral-8x7B-Instruct-v0.1"),
|
||||||
|
Self::GPT4OMini => String::from("gpt-4o-mini")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +50,27 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
||||||
|
pub fn set_tos(&mut self, tos: bool) -> Result<(), Box<dyn Error>> {
|
||||||
|
self.tos = tos;
|
||||||
|
self.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_tos(&self) -> () {
|
||||||
|
if ! self.tos {
|
||||||
|
eprintln!("{RED}You need to agree to DuckDuckgo AI Chat TOS to continue.{RESET}");
|
||||||
|
eprintln!("{GRAY}Visit it on this URL: {RESET}{BLUE}https://duck.ai{RESET}");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("{GRAY}If it doesnt pop up, open the URL in a private window.{RESET}");
|
||||||
|
eprintln!("{GRAY}Running this command is equivalent to clicking \"I Agree\" in the ToS window:{RESET}");
|
||||||
|
eprintln!("{BLUE} {} --agree-tos{RESET}", std::env::current_exe().unwrap().display());
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("{WARN}Note: if you want to, modify `tos` parameter in {}{RESET}", Config::get_path::<PathBuf>().join(Config::get_file_name::<String>()).display());
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_path<T: From<String>>() -> T {
|
pub fn get_path<T: From<String>>() -> T {
|
||||||
match env::var("HEY_CONFIG_PATH") {
|
match env::var("HEY_CONFIG_PATH") {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -92,4 +117,4 @@ impl Config {
|
||||||
Ok(conf)
|
Ok(conf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::api::ChatMessagePayload;
|
||||||
|
use crate::{RESET, GREEN, GRAY, BLUE};
|
||||||
|
|
||||||
|
pub trait HistoryObject {
|
||||||
|
fn display_as_history(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HistoryObject for ChatMessagePayload {
|
||||||
|
fn display_as_history(&self) -> String {
|
||||||
|
let user = &self.role;
|
||||||
|
let msg = &self.content;
|
||||||
|
|
||||||
|
let role_color = {
|
||||||
|
if user == "user" { GREEN }
|
||||||
|
else if user == "assistant" { BLUE }
|
||||||
|
else { GRAY }
|
||||||
|
};
|
||||||
|
|
||||||
|
let short_user = {
|
||||||
|
if user == "user" { "you" }
|
||||||
|
else if user == "assistant" { "ai" }
|
||||||
|
else { user }
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
format!("{role_color}{short_user}{RESET}\t: {msg}{RESET}")
|
||||||
|
}
|
||||||
|
}
|
51
src/main.rs
51
src/main.rs
|
@ -1,28 +1,33 @@
|
||||||
use std::{path::PathBuf, process::exit};
|
use std::process::exit;
|
||||||
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use session::Session;
|
||||||
use std::io::{stdout, IsTerminal};
|
use std::io::{stdout, IsTerminal};
|
||||||
|
|
||||||
use crate::{cache::Cache, config::Config};
|
use crate::config::Config;
|
||||||
use crate::api::{get_res, get_vqd, simulate_browser_reqs};
|
use crate::api::{get_res, get_vqd, simulate_browser_reqs};
|
||||||
|
|
||||||
mod cache;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod api;
|
mod api;
|
||||||
|
mod session;
|
||||||
|
mod history;
|
||||||
|
|
||||||
pub const GREEN: &str = "\x1b[1;32m";
|
pub const GREEN: &str = "\x1b[1;32m";
|
||||||
pub const RED: &str = "\x1b[1;31m";
|
pub const RED: &str = "\x1b[1;31m";
|
||||||
pub const BLUE: &str = "\x1b[34m";
|
pub const BLUE: &str = "\x1b[34m";
|
||||||
pub const WARN: &str = "\x1b[33m";
|
pub const WARN: &str = "\x1b[33m";
|
||||||
pub const RESET: &str = "\x1b[0m";
|
pub const RESET: &str = "\x1b[0m";
|
||||||
|
pub const GRAY: &str = "\x1b[90m";
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(long, default_value = "false", required = false, help = "If you want to agree to the DuckDuckGo TOS")]
|
#[arg(long, default_value = "false", required = false, help = "If you want to agree to the DuckDuckGo TOS")]
|
||||||
pub agree_tos: bool,
|
pub agree_tos: bool,
|
||||||
|
#[arg(long, short, default_value = "false", required = false, help = "Clear current session")]
|
||||||
|
pub clear: bool,
|
||||||
#[arg()]
|
#[arg()]
|
||||||
pub query: Vec<String>,
|
pub query: Vec<String>,
|
||||||
}
|
}
|
||||||
|
@ -38,38 +43,44 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let query = args.query.join(" ");
|
let query = args.query.join(" ").trim().to_string();
|
||||||
|
|
||||||
|
if args.clear {
|
||||||
|
Session::clear();
|
||||||
|
println!("Cleared session");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let session = Session::create_or_restore("");
|
||||||
|
if session.is_restored() {
|
||||||
|
session.print_history();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.len() == 0 {
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
let mut cache = Cache::load().unwrap();
|
|
||||||
let mut config = Config::load().unwrap();
|
let mut config = Config::load().unwrap();
|
||||||
|
|
||||||
if args.agree_tos {
|
if args.agree_tos {
|
||||||
if ! config.tos {
|
if ! config.tos {
|
||||||
println!("{GREEN}TOS accepted{RESET}");
|
println!("{GREEN}TOS accepted{RESET}");
|
||||||
}
|
}
|
||||||
config.tos = true;
|
config.set_tos(true).expect("Error saving config");
|
||||||
config.save().expect("Error saving config");
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! config.tos {
|
config.check_tos();
|
||||||
eprintln!("{RED}You need to agree to duckduckgo AI chat TOS to continue.{RESET}");
|
|
||||||
eprintln!("{RED}Visit it on this URL: {RESET}{BLUE}https://duckduckgo.com/?q=duckduckgo+ai+chat&ia=chat{RESET}");
|
|
||||||
eprintln!("Once you read it, pass --agree-tos parameter to agree.");
|
|
||||||
eprintln!("{WARN}Note: if you want to, modify `tos` parameter in {}{RESET}", Config::get_path::<PathBuf>().join(Config::get_file_name::<String>()).display());
|
|
||||||
exit(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{GREEN}Contacting DuckDuckGo chat AI...{RESET}");
|
|
||||||
|
|
||||||
let cli = Client::new();
|
let cli = Client::new();
|
||||||
simulate_browser_reqs(&cli).await.unwrap();
|
simulate_browser_reqs(&cli).await.unwrap();
|
||||||
|
|
||||||
let vqd = match cache.get_last_vqd() {
|
let vqd = match Session::restore_vqd() {
|
||||||
Some(v) => { println!("using cached vqd"); v},
|
Some(v) => { v },
|
||||||
None => get_vqd(&cli).await.unwrap()
|
None => get_vqd(&cli).await.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
get_res(&cli, query, vqd, &mut cache, &config).await;
|
get_res(&cli, query, vqd, &config).await;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::api::{ChatChunk, ChatMessagePayload};
|
||||||
|
use crate::{GRAY, RESET};
|
||||||
|
use crate::history::HistoryObject;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
session_id: String,
|
||||||
|
ttl: DateTime<Local>,
|
||||||
|
messages: Vec<ChatMessagePayload>,
|
||||||
|
vqd: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
restored: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn terminal_session_id<T: From<String>>() -> T {
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn inner() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let child = Command::new("tty")
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let child = child.wait_with_output()?;
|
||||||
|
if ! child.status.success() {
|
||||||
|
Err("")? // value ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::from_utf8(child.stdout)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn inner() -> Result<String, ()> {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
inner()
|
||||||
|
.unwrap_or("default".into())
|
||||||
|
.replace(['/', '\\', ':'], "-")
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('-')
|
||||||
|
.to_string()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_for_id<T: Into<String>>(id: T) -> PathBuf {
|
||||||
|
let id: String = id.into();
|
||||||
|
std::env::temp_dir().join(format!("hey-duck-session-{id}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> PathBuf {
|
||||||
|
Self::path_for_id(&self.session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_with_id<T: Into<String>>(id: T) -> Option<Self> {
|
||||||
|
fn inner(path: PathBuf) -> Result<Session, Box<dyn std::error::Error>> {
|
||||||
|
if path.is_file() {
|
||||||
|
let data = fs::read_to_string(path)?;
|
||||||
|
Ok(serde_json::from_str(&data)?)
|
||||||
|
} else {
|
||||||
|
Err("")? // value ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match inner(Self::path_for_id(id)) {
|
||||||
|
Ok(mut session) => {
|
||||||
|
session.restored = true;
|
||||||
|
if session.is_expired() {
|
||||||
|
session.destroy().expect("Couldn't destroy expired session");
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
session.increase_ttl().expect("Couldn't increase TTL");
|
||||||
|
Some(session)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ttl() -> DateTime<Local> {
|
||||||
|
Local::now() + Duration::from_secs(60 * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increase_ttl(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.ttl = Self::get_ttl();
|
||||||
|
Ok(self.save()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new<I: Into<String>, V: Into<String>>(id: I, vqd: V) -> Self {
|
||||||
|
Self {
|
||||||
|
session_id: id.into(),
|
||||||
|
ttl: Self::get_ttl(),
|
||||||
|
messages: vec![],
|
||||||
|
vqd: vqd.into(),
|
||||||
|
|
||||||
|
restored: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if self.messages.len() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
fs::write(self.path(), serde_json::to_string_pretty(self)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_expired(&self) -> bool {
|
||||||
|
self.ttl < Local::now()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
let session = Session::create_or_restore("");
|
||||||
|
if session.is_restored() {
|
||||||
|
session.destroy().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_restored(&self) -> bool {
|
||||||
|
self.restored
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_history(&self) {
|
||||||
|
println!("{GRAY}* start restored conversation *{RESET}");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for message in self.messages.iter() {
|
||||||
|
println!("{}", message.display_as_history());
|
||||||
|
if message.role == "assistant" {
|
||||||
|
println!("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{GRAY}* end restored conversation *{RESET}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Ok(fs::remove_file(self.path())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_or_restore<T: Into<String>>(vqd: T) -> Self {
|
||||||
|
let session_id: String = Self::terminal_session_id();
|
||||||
|
match Self::restore_with_id(&session_id) {
|
||||||
|
Some(session) => { session },
|
||||||
|
None => {
|
||||||
|
let session = Self::new(&session_id, vqd);
|
||||||
|
session.save().expect("Couldn't save new session");
|
||||||
|
session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_message(&mut self, msg: &ChatMessagePayload) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.messages.push(msg.clone());
|
||||||
|
Ok(self.save()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_ai_message_chunk(&mut self, chunk: &ChatChunk) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if self.messages.last().unwrap().is_ai() {
|
||||||
|
self.messages.last_mut().unwrap().content += chunk.message.as_str();
|
||||||
|
} else {
|
||||||
|
self.messages.push(chunk.clone().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.save()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_messages(&self) -> Vec<ChatMessagePayload> {
|
||||||
|
self.messages.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_vqd() -> Option<String> {
|
||||||
|
Self::restore_with_id(Self::terminal_session_id::<String>()).map(|x| x.vqd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_last_vqd<T: Into<String>>(&mut self, vqd: T) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.vqd = vqd.into();
|
||||||
|
Ok(self.save()?)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue