PikoAi 0.1.15__tar.gz → 0.1.17__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.
Files changed (48) hide show
  1. {pikoai-0.1.15 → pikoai-0.1.17}/PKG-INFO +1 -1
  2. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Agents/Executor/executor.py +1 -16
  3. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Agents/Executor/prompts.py +5 -7
  4. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/PKG-INFO +1 -1
  5. pikoai-0.1.17/Src/Tools/system_details.py +92 -0
  6. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/tool_dir.json +1 -1
  7. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/tool_manager.py +1 -1
  8. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/userinp.py +1 -9
  9. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/web_loader.py +3 -21
  10. pikoai-0.1.17/Src/Tools/web_search.py +58 -0
  11. pikoai-0.1.17/Src/cli.py +265 -0
  12. {pikoai-0.1.15 → pikoai-0.1.17}/setup.py +1 -1
  13. pikoai-0.1.15/Src/Tools/system_details.py +0 -76
  14. pikoai-0.1.15/Src/Tools/web_search.py +0 -67
  15. pikoai-0.1.15/Src/cli.py +0 -362
  16. {pikoai-0.1.15 → pikoai-0.1.17}/LICENSE +0 -0
  17. {pikoai-0.1.15 → pikoai-0.1.17}/README.md +0 -0
  18. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Agents/Executor/__init__.py +0 -0
  19. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Agents/__init__.py +0 -0
  20. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/__init__.py +0 -0
  21. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/base_env.py +0 -0
  22. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/base_executor.py +0 -0
  23. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/env.py +0 -0
  24. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/js_executor.py +0 -0
  25. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/python_executor.py +0 -0
  26. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/shell.py +0 -0
  27. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/tests/__init__.py +0 -0
  28. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/tests/test_python_executor.py +0 -0
  29. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Env/tests/test_shell_executor.py +0 -0
  30. {pikoai-0.1.15 → pikoai-0.1.17}/Src/OpenCopilot.py +0 -0
  31. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/SOURCES.txt +0 -0
  32. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/dependency_links.txt +0 -0
  33. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/entry_points.txt +0 -0
  34. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/requires.txt +0 -0
  35. {pikoai-0.1.15 → pikoai-0.1.17}/Src/PikoAi.egg-info/top_level.txt +0 -0
  36. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/__init__.py +0 -0
  37. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Tools/file_task.py +0 -0
  38. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Utils/__init__.py +0 -0
  39. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Utils/executor_utils.py +0 -0
  40. {pikoai-0.1.15 → pikoai-0.1.17}/Src/Utils/ter_interface.py +0 -0
  41. {pikoai-0.1.15 → pikoai-0.1.17}/Src/llm_interface/__init__.py +0 -0
  42. {pikoai-0.1.15 → pikoai-0.1.17}/Src/llm_interface/llm.py +0 -0
  43. {pikoai-0.1.15 → pikoai-0.1.17}/setup.cfg +0 -0
  44. {pikoai-0.1.15 → pikoai-0.1.17}/test/test.py +0 -0
  45. {pikoai-0.1.15 → pikoai-0.1.17}/test/test_file_task.py +0 -0
  46. {pikoai-0.1.15 → pikoai-0.1.17}/test/test_opencopilot_file_integration.py +0 -0
  47. {pikoai-0.1.15 → pikoai-0.1.17}/test/testjs.py +0 -0
  48. {pikoai-0.1.15 → pikoai-0.1.17}/test/testscript.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PikoAi
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: An AI-powered task automation tool
5
5
  Home-page: https://github.com/nihaaaar22/OS-Assistant
6
6
  Author: Nihar S
@@ -9,16 +9,7 @@ from Utils.ter_interface import TerminalInterface
9
9
  from Utils.executor_utils import parse_tool_call
10
10
  from Agents.Executor.prompts import get_executor_prompt # Import prompts
11
11
 
12
- from typing import Optional
13
- from mistralai.models.sdkerror import SDKError # This might be an issue if LiteLLM doesn't use SDKError
14
- # LiteLLM maps exceptions to OpenAI exceptions.
15
- # We'll keep it for now and see if errors arise during testing.
16
- # from Env import python_executor # Will be replaced by BaseEnv
17
- # from Env.shell import ShellExecutor # Will be replaced by BaseEnv
18
- from Env.base_env import create_environment, BaseEnv # Added
19
- from Env import python_executor # Keep for type hint in the old execute method if needed, or remove if execute is fully removed
20
12
  from llm_interface.llm import LiteLLMInterface # Import LiteLLMInterface
21
-
22
13
  from Tools import tool_manager
23
14
 
24
15
  class RateLimiter:
@@ -93,12 +84,6 @@ class executor:
93
84
  retries += 1
94
85
  print(f"\nRate limit error detected. Waiting {self.rate_limiter.wait_time} seconds before retry {retries}/{self.rate_limiter.max_retries}")
95
86
  time.sleep(self.rate_limiter.wait_time)
96
- # Check if the error is an SDKError (though less likely with LiteLLM directly)
97
- # or if it's any other exception that we should retry or raise.
98
- elif isinstance(e, SDKError) and "429" in str(e) and retries < self.rate_limiter.max_retries: # Added SDKError check just in case
99
- retries += 1
100
- print(f"\nRate limit exceeded (SDKError). Waiting {self.rate_limiter.wait_time} seconds before retry {retries}/{self.rate_limiter.max_retries}")
101
- time.sleep(self.rate_limiter.wait_time)
102
87
  else:
103
88
  print(f"\nError occurred during inference: {str(e)}")
104
89
  # You might want to log the full traceback here for debugging
@@ -130,7 +115,7 @@ class executor:
130
115
  tool_output_result = tool_manager.call_tool(tool_name, tool_input)
131
116
  if tool_name not in ['execute_python_code', 'execute_shell_command']:
132
117
  self.terminal.tool_output_log(tool_output_result, tool_name)
133
- self.message.append({"role": "user", "content": tool_output_result})
118
+ self.message.append({"role": "user", "content": "Tool Output: " + str(tool_output_result)})
134
119
  except ValueError as e:
135
120
  error_msg = str(e)
136
121
  print(f"Tool Error: {error_msg}")
@@ -33,9 +33,7 @@ You must break down the user's goal into smaller steps and perform one action at
33
33
  }}
34
34
  }}
35
35
  <<END_TOOL_CALL>>
36
- - **Code Execution**: Write Python code when no tool is suitable or when custom logic is needed.
37
- the code written will be executed immediately and not saved.
38
- - **Direct Response**: Provide a direct answer if the task doesn't require tools or code.
36
+ - **Direct Response**: Provide a direct answer if the task doesn't require tool calling
39
37
 
40
38
 
41
39
  These are the things that you learned from the mistakes you made earlier :
@@ -46,13 +44,13 @@ These are the things that you learned from the mistakes you made earlier :
46
44
  - Don't execute dangerous commands like rm -rf * or access sensitive files
47
45
  - If you are stuck, have tried to fix an issue (e.g., a linter error) multiple times (e.g., 3 times) without success, or need clarification, ask the USER for input. Explain the situation clearly.
48
46
  - Upon creating anything (like a new project, website, data analysis png) always show the output.You can do this by executing shell commands.
49
- - the python/shell code execution in tool call will be executed immediately and output will be shown. it wont be saved.
47
+ - the python/shell code execution through tool call will be executed immediately and output will be shown. it wont be saved.
50
48
 
51
49
 
52
50
  ** Important **
53
- - Perform only one action per step (either a single tool call or a single code execution).
54
- - Always evaluate the output of each action before deciding the next step.
51
+ - You can only perform one tool call at a time.
52
+ - Always evaluate the output of the tool call before deciding the next step.
55
53
  - Continue performing actions until the user's goal is fully achieved. Only then, include 'TASK_DONE' in your response if that is the required signal for completion.
56
- - Do not end the task immediately after a tool call or code execution without evaluating its output.
54
+ - Do not end the task immediately after a tool call without evaluating its output.
57
55
 
58
56
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PikoAi
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: An AI-powered task automation tool
5
5
  Home-page: https://github.com/nihaaaar22/OS-Assistant
6
6
  Author: Nihar S
@@ -0,0 +1,92 @@
1
+ import platform
2
+ import psutil
3
+ import datetime
4
+
5
+ def get_os_details():
6
+ """Get operating system details"""
7
+ os_info = {
8
+ "system": platform.system(),
9
+ "release": platform.release(),
10
+ "version": platform.version(),
11
+ "machine": platform.machine(),
12
+ "processor": platform.processor()
13
+ }
14
+
15
+ return f"""Operating System Details:
16
+ System: {os_info['system']}
17
+ Release: {os_info['release']}
18
+ Version: {os_info['version']}
19
+ Machine: {os_info['machine']}
20
+ Processor: {os_info['processor']}"""
21
+
22
+ def get_datetime():
23
+ """Get current date and time"""
24
+ now = datetime.datetime.now()
25
+ date = now.strftime("%Y-%m-%d")
26
+ time = now.strftime("%H:%M:%S")
27
+ timezone = datetime.datetime.now().astimezone().tzname()
28
+
29
+ return f"""Date and Time:
30
+ Date: {date}
31
+ Time: {time}
32
+ Timezone: {timezone}"""
33
+
34
+ def get_memory_usage():
35
+ """Get memory usage details"""
36
+ memory = psutil.virtual_memory()
37
+ total = f"{memory.total / (1024**3):.2f} GB"
38
+ available = f"{memory.available / (1024**3):.2f} GB"
39
+ used = f"{memory.used / (1024**3):.2f} GB"
40
+ percent = f"{memory.percent}%"
41
+
42
+ return f"""Memory Usage:
43
+ Total: {total}
44
+ Available: {available}
45
+ Used: {used}
46
+ Usage Percent: {percent}"""
47
+
48
+ def get_cpu_info():
49
+ """Get CPU information"""
50
+ cpu_freq = psutil.cpu_freq()
51
+ current_freq = f"{cpu_freq.current:.2f} MHz" if cpu_freq else "N/A"
52
+ min_freq = f"{cpu_freq.min:.2f} MHz" if cpu_freq and cpu_freq.min else "N/A"
53
+ max_freq = f"{cpu_freq.max:.2f} MHz" if cpu_freq and cpu_freq.max else "N/A"
54
+
55
+ return f"""CPU Information:
56
+ Physical Cores: {psutil.cpu_count(logical=False)}
57
+ Total Cores: {psutil.cpu_count(logical=True)}
58
+ Current Frequency: {current_freq}
59
+ Min Frequency: {min_freq}
60
+ Max Frequency: {max_freq}
61
+ CPU Usage: {psutil.cpu_percent()}%"""
62
+
63
+ def system_details(detail_type="all"):
64
+ """
65
+ Get system details based on the requested type.
66
+
67
+ Args:
68
+ detail_type (str): Type of system detail to retrieve (os, datetime, memory, cpu, all)
69
+
70
+ Returns:
71
+ str: Requested system details as formatted string
72
+ """
73
+ detail_type = detail_type.lower()
74
+
75
+ if detail_type == "all":
76
+ return f"""{get_os_details()}
77
+
78
+ {get_datetime()}
79
+
80
+ {get_memory_usage()}
81
+
82
+ {get_cpu_info()}"""
83
+ elif detail_type == "os":
84
+ return get_os_details()
85
+ elif detail_type == "datetime":
86
+ return get_datetime()
87
+ elif detail_type == "memory":
88
+ return get_memory_usage()
89
+ elif detail_type == "cpu":
90
+ return get_cpu_info()
91
+ else:
92
+ raise ValueError(f"Invalid detail type: {detail_type}. Must be one of: os, datetime, memory, cpu, all")
@@ -68,7 +68,7 @@
68
68
  "summary": "Prompts the user for input and returns their response",
69
69
  "arguments": {
70
70
  "prompt": "The message to display to the user",
71
- "input_type": "Type of input to validate (text, number, boolean, optional, defaults to text)"
71
+ "input_type": "Type of input to validate (text, number, optional, defaults to text)"
72
72
  }
73
73
  },
74
74
  {
@@ -77,7 +77,7 @@ def call_tool(tool_name, tool_input):
77
77
  # Pass the tool_input dictionary as kwargs to the tool function
78
78
  return tools_function_map[tool_name](**tool_input)
79
79
  else:
80
- raise ValueError(f"Tool '{tool_name}' not found. Check the tools available in the tool directory")
80
+ return f"This tool is invalid. Please check the tools available in the tool directory"
81
81
 
82
82
  tools_function_map = {
83
83
  "web_loader": load_data,
@@ -17,15 +17,7 @@ def get_user_input(prompt="Enter input: ", input_type="text"):
17
17
  return user_input
18
18
  elif input_type == "number":
19
19
  return float(user_input)
20
- elif input_type == "boolean":
21
- user_input = user_input.lower()
22
- if user_input in ["true", "yes", "y", "1"]:
23
- return True
24
- elif user_input in ["false", "no", "n", "0"]:
25
- return False
26
- else:
27
- print("Please enter 'yes' or 'no'")
28
- continue
20
+
29
21
  else:
30
22
  raise ValueError(f"Invalid input type: {input_type}")
31
23
 
@@ -123,7 +123,6 @@ def load_data(**kwargs):
123
123
  headers = {
124
124
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36"
125
125
  }
126
- web_data = {}
127
126
  content = ""
128
127
  try:
129
128
  response = session.get(url, headers=headers, timeout=30)
@@ -139,28 +138,11 @@ def load_data(**kwargs):
139
138
  # Extract text from each page and combine it
140
139
  content = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
141
140
 
142
- meta_data = {"url": url}
143
- doc_id = hashlib.sha256((content + url).encode()).hexdigest()
144
- web_data = {
145
- "doc_id": doc_id,
146
- "data": [
147
- {
148
- "content": content,
149
- "meta_data": meta_data,
150
- }
151
- ],
152
- }
153
141
  except Exception as e:
154
142
  logging.error(f"Error loading data from {url}: {e}")
155
- web_data = {
156
- "data": [
157
- {
158
- "content": "",
159
- "meta_data": "",
160
- }
161
- ],
162
- }
163
- return web_data
143
+ content = ""
144
+
145
+ return content
164
146
 
165
147
 
166
148
  def close_session(session):
@@ -0,0 +1,58 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from duckduckgo_search import DDGS
4
+ import requests
5
+
6
+ load_dotenv()
7
+
8
+ def web_search(max_results: int = 10, **kwargs) -> str:
9
+ """
10
+ Performs a DuckDuckGo web search based on your query (think a Google search) then returns the top search results.
11
+ Falls back to SerpAPI (Google) if DuckDuckGo fails or returns no results.
12
+
13
+ Args:
14
+ query (str): The search query to perform.
15
+ max_results (int, optional): Maximum number of results to return. Defaults to 10.
16
+ **kwargs: Additional keyword arguments to pass to DDGS.
17
+
18
+ Returns:
19
+ str: Formatted string containing search results.
20
+
21
+ Raises:
22
+ ImportError: If neither duckduckgo_search nor SerpAPI is available.
23
+ Exception: If no results are found for the given query.
24
+ """
25
+ query = kwargs['query']
26
+ # Try DuckDuckGo first
27
+ try:
28
+ ddgs = DDGS()
29
+ results = ddgs.text(query, max_results=max_results)
30
+ if results and len(results) > 0:
31
+ postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
32
+ return "## Search Results (DuckDuckGo)\n\n" + "\n\n".join(postprocessed_results)
33
+ except Exception:
34
+ pass # Will try SerpAPI fallback
35
+
36
+ # Fallback to SerpAPI (Google)
37
+ SERP_API_KEY = os.getenv("SERP_API_KEY")
38
+ if not SERP_API_KEY:
39
+ raise ImportError("DuckDuckGo search failed and SERP_API_KEY is not set for SerpAPI fallback.")
40
+ url = "https://serpapi.com/search"
41
+ params = {
42
+ "q": query,
43
+ "api_key": SERP_API_KEY,
44
+ "engine": "google"
45
+ }
46
+ try:
47
+ response = requests.get(url, params=params)
48
+ data = response.json()
49
+ results = data.get("organic_results", [])
50
+ if not results:
51
+ raise Exception("No results found from SerpAPI either!")
52
+ simplified_results = [
53
+ f"[{result.get('title')}]({result.get('link')})\n{result.get('snippet', '')}"
54
+ for result in results[:max_results]
55
+ ]
56
+ return "## Search Results (Google via SerpAPI)\n\n" + "\n\n".join(simplified_results)
57
+ except Exception as e:
58
+ raise Exception(f"No results found from DuckDuckGo or SerpAPI. Last error: {e}")
@@ -0,0 +1,265 @@
1
+ import click
2
+ import json
3
+ import os
4
+ import inquirer
5
+ import shutil
6
+ from OpenCopilot import OpenCopilot
7
+ from dotenv import load_dotenv
8
+
9
+ # Define available models for each provider using litellm compatible strings
10
+ AVAILABLE_MODELS = {
11
+ "openai": [
12
+ "openai/gpt-3.5-turbo",
13
+ "openai/gpt-4",
14
+ "openai/gpt-4-turbo-preview",
15
+ "openai/gpt-4o",
16
+ "openai/gpt-4o-mini",
17
+ "openai/gpt-4.1-nano",
18
+ "openai/gpt-4.1-mini"
19
+ ],
20
+ "mistral": [
21
+ "mistral/mistral-tiny",
22
+ "mistral/mistral-small",
23
+ "mistral/mistral-medium",
24
+ "mistral/mistral-large-latest"
25
+ ],
26
+ "groq": [
27
+ "groq/llama2-70b-4096",
28
+ "groq/mixtral-8x7b-32768",
29
+ "groq/gemma-7b-it"
30
+ ],
31
+ "anthropic": [
32
+ "anthropic/claude-3-opus-20240229",
33
+ "anthropic/claude-3-sonnet-20240229",
34
+ "anthropic/claude-3-haiku-20240307"
35
+ ]
36
+ }
37
+
38
+ # Define API key environment variables for each provider (matching litellm conventions)
39
+ API_KEYS = {
40
+ "openai": "OPENAI_API_KEY",
41
+ "mistral": "MISTRAL_API_KEY",
42
+ "groq": "GROQ_API_KEY",
43
+ "anthropic": "ANTHROPIC_API_KEY"
44
+ }
45
+
46
+ # --- Utility Functions ---
47
+
48
+ def clear_terminal():
49
+ """Clear the terminal screen."""
50
+ os.system('cls' if os.name == 'nt' else 'clear')
51
+
52
+ def get_provider_from_model_name(model_name: str) -> str:
53
+ """Extracts the provider from a litellm model string (e.g., 'openai/gpt-4o' -> 'openai')."""
54
+ if not model_name or '/' not in model_name:
55
+ print(f"Warning: Model name '{model_name}' may not be in 'provider/model' format. Attempting to use as provider.")
56
+ return model_name
57
+ return model_name.split('/')[0]
58
+
59
+ # --- Configuration Management ---
60
+
61
+ def load_config(config_path: str) -> dict:
62
+ """Load (or create) config.json and return its contents as a dict. If config.json does not exist, copy from config.example.json (or create a default) and update working_directory to os.getcwd()."""
63
+ if not os.path.exists(config_path):
64
+ example_path = os.path.join(os.path.dirname(__file__), '../config.example.json')
65
+ if os.path.exists(example_path):
66
+ shutil.copy2(example_path, config_path)
67
+ with open(config_path, 'r') as f:
68
+ config = json.load(f)
69
+ else:
70
+ config = { "working_directory": os.getcwd(), "llm_provider": None, "model_name": None }
71
+ else:
72
+ with open(config_path, 'r') as f:
73
+ try:
74
+ config = json.load(f)
75
+ config["working_directory"] = os.getcwd()
76
+ except json.JSONDecodeError:
77
+ print("Error reading config.json. File might be corrupted. Re-creating default.")
78
+ config = { "working_directory": os.getcwd(), "llm_provider": None, "model_name": None }
79
+ # Always update working_directory to current directory
80
+ config["working_directory"] = os.getcwd()
81
+ return config
82
+
83
+ def save_config(config_path: str, config: dict) -> None:
84
+ """Save config dict (with updated working_directory) to config_path."""
85
+ with open(config_path, 'w') as f:
86
+ json.dump(config, f, indent=4)
87
+
88
+ # --- API Key Management ---
89
+
90
+ def ensure_api_key(provider: str):
91
+ """Ensure that an API key for the given provider (e.g. "openai") is available (via .env or prompt) and return it. Raise an error if unknown provider."""
92
+ env_path = os.path.join(os.path.dirname(__file__), '../.env')
93
+ env_var = API_KEYS.get(provider)
94
+ if not env_var:
95
+ raise ValueError(f"Unknown provider: {provider}")
96
+
97
+ # Force reload .env (if it exists) so that any new key is picked up.
98
+ if os.path.exists(env_path):
99
+ load_dotenv(env_path, override=True)
100
+
101
+ api_key = os.getenv(env_var)
102
+ if not api_key:
103
+ questions = [ inquirer.Text("api_key", message=f"Enter your {provider.upper()} API key", validate=lambda _, x: len(x.strip()) > 0) ]
104
+ api_key = inquirer.prompt(questions)["api_key"]
105
+ clear_terminal()
106
+ # Save (or update) the key in .env
107
+ lines = []
108
+ if os.path.exists(env_path):
109
+ with open(env_path, 'r') as f:
110
+ lines = f.readlines()
111
+ key_line = f"{env_var}={api_key}\n"
112
+ key_exists = False
113
+ for (i, line) in enumerate(lines):
114
+ if (line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}=")):
115
+ lines[i] = key_line
116
+ key_exists = True
117
+ break
118
+ if not key_exists:
119
+ lines.append(key_line)
120
+ with open(env_path, 'w') as f:
121
+ f.writelines(lines)
122
+ # Reload .env (override) so that the new key is available.
123
+ load_dotenv(env_path, override=True)
124
+
125
+
126
+ # --- Model / Provider Management ---
127
+
128
+ def prompt_model_selection() -> tuple:
129
+ """Prompt the user (via inquirer) to select a provider (from AVAILABLE_MODELS) and then a model (from that provider's list). Return (provider, model_name_full)."""
130
+ questions = [ inquirer.List("provider_key", message="Select LLM Provider", choices=list(AVAILABLE_MODELS.keys())) ]
131
+ selected_provider_key = inquirer.prompt(questions)["provider_key"]
132
+ clear_terminal()
133
+ # (Ensure API key for the selected provider.)
134
+ ensure_api_key(selected_provider_key)
135
+ questions = [ inquirer.List("model_name_full", message=f"Select {selected_provider_key} Model", choices=AVAILABLE_MODELS[selected_provider_key]) ]
136
+ selected_model_name_full = inquirer.prompt(questions)["model_name_full"]
137
+ clear_terminal()
138
+ return (selected_provider_key, selected_model_name_full)
139
+
140
+ def update_model_config(config_path: str, provider_key: str, model_name_full: str) -> None:
141
+ """Update config (at config_path) so that "llm_provider" is provider_key and "model_name" is model_name_full. (Also update "working_directory" to os.getcwd() if missing.)"""
142
+ config = load_config(config_path)
143
+ config["llm_provider"] = provider_key
144
+ config["model_name"] = model_name_full
145
+ if "working_directory" not in config or not config["working_directory"]:
146
+ config["working_directory"] = os.getcwd()
147
+ save_config(config_path, config)
148
+
149
+ # --- CLI Commands ---
150
+
151
+ @click.group(invoke_without_command=True)
152
+ @click.option("--task", "-t", help="The task to automate")
153
+ @click.option("--max-iter", "-m", default=10, help="Maximum number of iterations for the task")
154
+ @click.option("--change-model", is_flag=True, help="Change the LLM provider and model")
155
+ @click.pass_context
156
+ def cli(ctx, task, max_iter, change_model):
157
+ """TaskAutomator – Your AI Task Automation Tool"""
158
+ config_path = os.path.join(os.path.dirname(__file__), '../config.json')
159
+ config = load_config(config_path)
160
+ save_config(config_path, config)
161
+
162
+ clear_terminal()
163
+
164
+ if change_model or not config.get("model_name"):
165
+ (provider, model) = prompt_model_selection()
166
+ update_model_config(config_path, provider, model)
167
+ click.echo(f"Model changed to {model}")
168
+ if not change_model: # Only return if this was triggered by missing model
169
+ return
170
+
171
+ # Ensure API key for the configured model (or derive provider from model_name if missing) before running OpenCopilot.
172
+ current_provider = config.get("llm_provider")
173
+ if not current_provider and config.get("model_name"):
174
+ current_provider = get_provider_from_model_name(config["model_name"])
175
+ if current_provider:
176
+ ensure_api_key(current_provider)
177
+ else:
178
+ click.echo("Error: LLM provider not configured. Please run with --change-model to set it up.", err=True)
179
+ return
180
+
181
+ copilot = OpenCopilot()
182
+ if ctx.invoked_subcommand is None:
183
+ if task:
184
+ copilot.run_task(user_prompt=task, max_iter=max_iter)
185
+ else:
186
+ copilot.run()
187
+
188
+ @cli.command("list-tools")
189
+ def list_tools():
190
+ """List all available automation tools."""
191
+ tools = OpenCopilot.list_available_tools()
192
+ click.echo("Available Tools:")
193
+ for tool in tools:
194
+ click.echo(f"- {tool['name']}: {tool['summary']}")
195
+ if tool.get("arguments"):
196
+ click.echo(f" Arguments: {tool['arguments']}")
197
+
198
+ @cli.command("list-models")
199
+ def list_models():
200
+ """List all available LLM providers and their models (litellm compatible)."""
201
+ click.echo("Available LLM Providers and Models (litellm compatible):")
202
+ for (provider_key, model_list) in AVAILABLE_MODELS.items():
203
+ click.echo(f"\n{provider_key.upper()}:")
204
+ for model_name_full in model_list:
205
+ click.echo(f" - {model_name_full}")
206
+
207
+ @cli.command("set-api-key")
208
+ @click.option("--provider", "-p", type=click.Choice(list(AVAILABLE_MODELS.keys())), help="The LLM provider to set API key for")
209
+ @click.option("--key", "-k", help="The API key to set (if not provided, will prompt for it)")
210
+ def set_api_key(provider, key):
211
+ """Set or update API key for a specific LLM provider."""
212
+ if not provider:
213
+ questions = [ inquirer.List("provider_key", message="Select LLM Provider to update API key", choices=list(AVAILABLE_MODELS.keys())) ]
214
+ provider = inquirer.prompt(questions)["provider_key"]
215
+ env_var = API_KEYS.get(provider)
216
+ if not env_var:
217
+ raise ValueError(f"Unknown provider: {provider}")
218
+ if not key:
219
+ questions = [ inquirer.Text("api_key", message=f"Enter your {provider.upper()} API key", validate=lambda _, x: len(x.strip()) > 0) ]
220
+ key = inquirer.prompt(questions)["api_key"]
221
+ env_path = os.path.join(os.path.dirname(__file__), '../.env')
222
+ lines = []
223
+ if os.path.exists(env_path):
224
+ with open(env_path, 'r') as f:
225
+ lines = f.readlines()
226
+ key_line = f"{env_var}={key}\n"
227
+ key_exists = False
228
+ for (i, line) in enumerate(lines):
229
+ if (line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}=")):
230
+ lines[i] = key_line
231
+ key_exists = True
232
+ break
233
+ if not key_exists:
234
+ lines.append(key_line)
235
+ with open(env_path, 'w') as f:
236
+ f.writelines(lines)
237
+ click.echo(f"API key for {provider.upper()} has been updated successfully in {env_path}")
238
+
239
+ @cli.command("set-serp-key")
240
+ @click.option("--key", "-k", help="The SERP API key to set (if not provided, will prompt for it)")
241
+ def set_serp_key(key):
242
+ """Set or update the SERP API key used for web search functionality."""
243
+ if not key:
244
+ questions = [ inquirer.Text("api_key", message="Enter your SERP API key", validate=lambda _, x: len(x.strip()) > 0) ]
245
+ key = inquirer.prompt(questions)["api_key"]
246
+ env_path = os.path.join(os.path.dirname(__file__), '../.env')
247
+ lines = []
248
+ if os.path.exists(env_path):
249
+ with open(env_path, 'r') as f:
250
+ lines = f.readlines()
251
+ key_line = f"SERP_API_KEY={key}\n"
252
+ key_exists = False
253
+ for (i, line) in enumerate(lines):
254
+ if (line.strip().startswith("SERP_API_KEY=") or line.strip().startswith("#SERP_API_KEY=")):
255
+ lines[i] = key_line
256
+ key_exists = True
257
+ break
258
+ if not key_exists:
259
+ lines.append(key_line)
260
+ with open(env_path, 'w') as f:
261
+ f.writelines(lines)
262
+ click.echo(f"SERP API key has been updated successfully in {env_path}")
263
+
264
+ if __name__ == '__main__':
265
+ cli()
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
 
4
4
  setup(
5
5
  name="PikoAi",
6
- version="0.1.15",
6
+ version="0.1.17",
7
7
  packages=find_packages(where="Src"),
8
8
  py_modules=["cli", "OpenCopilot"],
9
9
  package_dir={"": "Src"},
@@ -1,76 +0,0 @@
1
- import platform
2
- import psutil
3
- import datetime
4
- import json
5
-
6
- def get_os_details():
7
- """Get operating system details"""
8
- return {
9
- "system": platform.system(),
10
- "release": platform.release(),
11
- "version": platform.version(),
12
- "machine": platform.machine(),
13
- "processor": platform.processor()
14
- }
15
-
16
- def get_datetime():
17
- """Get current date and time"""
18
- now = datetime.datetime.now()
19
- return {
20
- "date": now.strftime("%Y-%m-%d"),
21
- "time": now.strftime("%H:%M:%S"),
22
- "timezone": datetime.datetime.now().astimezone().tzname()
23
- }
24
-
25
- def get_memory_usage():
26
- """Get memory usage details"""
27
- memory = psutil.virtual_memory()
28
- return {
29
- "total": f"{memory.total / (1024**3):.2f} GB",
30
- "available": f"{memory.available / (1024**3):.2f} GB",
31
- "used": f"{memory.used / (1024**3):.2f} GB",
32
- "percent": f"{memory.percent}%"
33
- }
34
-
35
- def get_cpu_info():
36
- """Get CPU information"""
37
- return {
38
- "physical_cores": psutil.cpu_count(logical=False),
39
- "total_cores": psutil.cpu_count(logical=True),
40
- "cpu_freq": {
41
- "current": f"{psutil.cpu_freq().current:.2f} MHz",
42
- "min": f"{psutil.cpu_freq().min:.2f} MHz",
43
- "max": f"{psutil.cpu_freq().max:.2f} MHz"
44
- },
45
- "cpu_usage": f"{psutil.cpu_percent()}%"
46
- }
47
-
48
- def system_details(detail_type="all"):
49
- """
50
- Get system details based on the requested type.
51
-
52
- Args:
53
- detail_type (str): Type of system detail to retrieve (os, datetime, memory, cpu, all)
54
-
55
- Returns:
56
- dict: Requested system details
57
- """
58
- detail_type = detail_type.lower()
59
-
60
- if detail_type == "all":
61
- return {
62
- "os": get_os_details(),
63
- "datetime": get_datetime(),
64
- "memory": get_memory_usage(),
65
- "cpu": get_cpu_info()
66
- }
67
- elif detail_type == "os":
68
- return get_os_details()
69
- elif detail_type == "datetime":
70
- return get_datetime()
71
- elif detail_type == "memory":
72
- return get_memory_usage()
73
- elif detail_type == "cpu":
74
- return get_cpu_info()
75
- else:
76
- raise ValueError(f"Invalid detail type: {detail_type}. Must be one of: os, datetime, memory, cpu, all")
@@ -1,67 +0,0 @@
1
- import os
2
- from duckduckgo_search import DDGS
3
- from serpapi import SerpApiClient
4
-
5
- def web_search(max_results: int = 10, **kwargs) -> str:
6
- """
7
- Performs a DuckDuckGo web search based on your query (think a Google search) then returns the top search results.
8
- If DuckDuckGo search fails, it falls back to SerpAPI.
9
-
10
- Args:
11
- query (str): The search query to perform.
12
- max_results (int, optional): Maximum number of results to return. Defaults to 10.
13
- **kwargs: Additional keyword arguments to pass to DDGS.
14
-
15
- Returns:
16
- str: Formatted string containing search results.
17
-
18
- Raises:
19
- ImportError: If the duckduckgo_search or serpapi package is not installed.
20
- Exception: If no results are found for the given query via both DuckDuckGo and SerpAPI, or if the SerpAPI key is not found.
21
-
22
- Note:
23
- For SerpAPI fallback, the SERPAPI_API_KEY environment variable must be set.
24
- """
25
- try:
26
- ddgs_instance = DDGS()
27
- except ImportError as e:
28
- raise ImportError("You must install package `duckduckgo_search` to run this function: for instance run `pip install duckduckgo-search`.") from e
29
-
30
- try:
31
- query = kwargs['query']
32
- results = ddgs_instance.text(query, max_results=max_results)
33
- if len(results) == 0:
34
- raise Exception("No results found via DuckDuckGo.")
35
-
36
- postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
37
-
38
- return "## Search Results (via DuckDuckGo)\n\n" + "\n\n".join(postprocessed_results)
39
-
40
- except Exception as e:
41
- # print(f"DuckDuckGo search failed: {e}. Falling back to SerpAPI.")
42
- # If the exception was the specific DDGS ImportError, we re-raise it directly if it wasn't caught above.
43
- # However, the structure above should prevent it from reaching here.
44
- # The primary purpose of this block is to catch runtime errors from ddgs.text or the "No results" exception.
45
-
46
- api_key = os.environ.get("SERPAPI_API_KEY")
47
- if not api_key:
48
- raise Exception("SerpAPI key not found. Please set the SERPAPI_API_KEY environment variable.")
49
-
50
- try:
51
- client = SerpApiClient({"api_key": api_key})
52
- except ImportError as serp_e:
53
- raise ImportError("You must install package `serpapi` to run this function: for instance run `pip install google-search-results`.") from serp_e
54
-
55
- search_params = {
56
- "engine": "google",
57
- "q": query,
58
- "num": max_results # SerpAPI uses 'num' for number of results
59
- }
60
- serp_results = client.search(search_params)
61
-
62
- if "organic_results" in serp_results and serp_results["organic_results"]:
63
- organic_results = serp_results["organic_results"]
64
- postprocessed_results = [f"[{result['title']}]({result['link']})\n{result.get('snippet', '')}" for result in organic_results]
65
- return "## Search Results (via SerpAPI)\n\n" + "\n\n".join(postprocessed_results)
66
- else:
67
- raise Exception(f"No results found via DuckDuckGo or SerpAPI! Original error: {e}")
pikoai-0.1.15/Src/cli.py DELETED
@@ -1,362 +0,0 @@
1
- import click
2
- import json
3
- import os
4
- import inquirer
5
- import shutil
6
- from OpenCopilot import OpenCopilot
7
- from dotenv import load_dotenv
8
-
9
- # Define available models for each provider using litellm compatible strings
10
- AVAILABLE_MODELS = {
11
- "openai": [
12
- "openai/gpt-3.5-turbo",
13
- "openai/gpt-4",
14
- "openai/gpt-4-turbo-preview",
15
- "openai/gpt-4o",
16
- "openai/gpt-4o-mini",
17
- "openai/gpt-4.1-nano",
18
- "openai/gpt-4.1-mini"
19
- ],
20
- "mistral": [
21
- "mistral/mistral-tiny",
22
- "mistral/mistral-small",
23
- "mistral/mistral-medium",
24
- "mistral/mistral-large-latest"
25
- ],
26
- "groq": [
27
- "groq/llama2-70b-4096",
28
- "groq/mixtral-8x7b-32768",
29
- "groq/gemma-7b-it"
30
- ],
31
- "anthropic": [
32
- "anthropic/claude-3-opus-20240229",
33
- "anthropic/claude-3-sonnet-20240229",
34
- "anthropic/claude-3-haiku-20240307"
35
- ]
36
- }
37
-
38
- # Define API key environment variables for each provider (matching litellm conventions)
39
- API_KEYS = {
40
- "openai": "OPENAI_API_KEY",
41
- "mistral": "MISTRAL_API_KEY",
42
- "groq": "GROQ_API_KEY",
43
- "anthropic": "ANTHROPIC_API_KEY"
44
- }
45
-
46
- def get_provider_from_model_name(model_name: str) -> str:
47
- """Extracts the provider from a litellm model string (e.g., 'openai/gpt-4o' -> 'openai')."""
48
- if not model_name or '/' not in model_name:
49
- # Fallback or error handling if model_name is not in expected format
50
- # For now, try to return the model_name itself if it doesn't contain '/',
51
- # as it might be a provider name already or an old format.
52
- # This case should ideally be handled based on how robust the system needs to be.
53
- print(f"Warning: Model name '{model_name}' may not be in 'provider/model' format. Attempting to use as provider.")
54
- return model_name
55
- return model_name.split('/')[0]
56
-
57
- def clear_terminal():
58
- """Clear the terminal screen"""
59
- os.system('cls' if os.name == 'nt' else 'clear')
60
-
61
- def ensure_api_key(provider):
62
- """Ensure API key exists for the given provider"""
63
- env_path = os.path.join(os.path.dirname(__file__), '../.env')
64
- env_var = API_KEYS.get(provider)
65
- if not env_var:
66
- raise ValueError(f"Unknown provider: {provider}")
67
-
68
- # Force reload of .env file
69
- if os.path.exists(env_path):
70
- load_dotenv(env_path, override=True)
71
-
72
- # Check if API key exists in environment
73
- api_key = os.getenv(env_var)
74
-
75
- if not api_key:
76
- # Ask for API key
77
- questions = [
78
- inquirer.Text('api_key',
79
- message=f"Enter your {provider.upper()} API key",
80
- validate=lambda _, x: len(x.strip()) > 0
81
- )
82
- ]
83
- api_key = inquirer.prompt(questions)['api_key']
84
- clear_terminal()
85
-
86
- # Save to .env file
87
- if not os.path.exists(env_path):
88
- with open(env_path, 'w') as f:
89
- f.write(f"{env_var}={api_key}\n")
90
- else:
91
- # Read existing .env file
92
- with open(env_path, 'r') as f:
93
- lines = f.readlines()
94
-
95
- # Check if key already exists
96
- key_exists = False
97
- for i, line in enumerate(lines):
98
- if line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}="):
99
- lines[i] = f"{env_var}={api_key}\n"
100
- key_exists = True
101
- break
102
-
103
- # If key doesn't exist, append it
104
- if not key_exists:
105
- lines.append(f"{env_var}={api_key}\n")
106
-
107
- # Write back to .env file
108
- with open(env_path, 'w') as f:
109
- f.writelines(lines)
110
-
111
- # Reload environment with new key
112
- load_dotenv(env_path, override=True)
113
-
114
- return api_key
115
-
116
- def ensure_config_exists():
117
- """Ensure config.json exists and has required fields"""
118
- config_path = os.path.join(os.path.dirname(__file__), '../config.json')
119
- config = None
120
-
121
- if not os.path.exists(config_path):
122
- # Copy config from example if it doesn't exist
123
- example_path = os.path.join(os.path.dirname(__file__), '../config.example.json')
124
- if os.path.exists(example_path):
125
- shutil.copy2(example_path, config_path)
126
- with open(config_path, 'r') as f:
127
- config = json.load(f)
128
- else:
129
- # Create a default config if example is missing
130
- config = {
131
- "working_directory": os.getcwd(),
132
- "llm_provider": None, # Will store the provider part, e.g., "openai"
133
- "model_name": None # Will store the full litellm string, e.g., "openai/gpt-4o"
134
- }
135
- else:
136
- # Read existing config
137
- with open(config_path, 'r') as f:
138
- try:
139
- config = json.load(f)
140
- except json.JSONDecodeError:
141
- print("Error reading config.json. File might be corrupted. Re-creating default.")
142
- config = {
143
- "working_directory": os.getcwd(),
144
- "llm_provider": None,
145
- "model_name": None
146
- }
147
-
148
- # Ensure 'working_directory' exists, default if not
149
- if "working_directory" not in config or not config["working_directory"]:
150
- config["working_directory"] = os.getcwd()
151
-
152
- # Check if model configuration is needed
153
- if not config.get("model_name") or not config.get("llm_provider"):
154
- print("LLM provider or model not configured.")
155
- questions = [
156
- inquirer.List('provider_key',
157
- message="Select LLM Provider",
158
- choices=list(AVAILABLE_MODELS.keys()) # User selects "openai", "mistral", etc.
159
- )
160
- ]
161
- selected_provider_key = inquirer.prompt(questions)['provider_key']
162
- clear_terminal()
163
-
164
- # Ensure API key exists for the selected provider
165
- ensure_api_key(selected_provider_key) # Uses "openai", "mistral", etc.
166
-
167
- questions = [
168
- inquirer.List('model_name_full',
169
- message=f"Select {selected_provider_key} Model",
170
- choices=AVAILABLE_MODELS[selected_provider_key] # Shows "openai/gpt-3.5-turbo", etc.
171
- )
172
- ]
173
- selected_model_name_full = inquirer.prompt(questions)['model_name_full']
174
- clear_terminal()
175
-
176
- config["llm_provider"] = selected_provider_key # Store "openai"
177
- config["model_name"] = selected_model_name_full # Store "openai/gpt-4o"
178
-
179
- with open(config_path, 'w') as f:
180
- json.dump(config, f, indent=4)
181
- print(f"Configuration saved: Provider '{selected_provider_key}', Model '{selected_model_name_full}'")
182
-
183
- else:
184
- # Config exists, ensure API key for the stored provider
185
- # llm_provider should already be the provider part, e.g., "openai"
186
- # If old config only had model_name, try to parse provider from it
187
- provider_to_check = config.get("llm_provider")
188
- if not provider_to_check and config.get("model_name"):
189
- provider_to_check = get_provider_from_model_name(config["model_name"])
190
- # Optionally, update config if llm_provider was missing
191
- if provider_to_check != config.get("llm_provider"): # Check if it's different or was None
192
- config["llm_provider"] = provider_to_check
193
- with open(config_path, 'w') as f:
194
- json.dump(config, f, indent=4)
195
-
196
- if provider_to_check:
197
- ensure_api_key(provider_to_check)
198
- else:
199
- # This case should ideally be handled by the initial setup logic
200
- print("Warning: Could not determine LLM provider from config to ensure API key.")
201
-
202
-
203
- # Create config file if it was created from scratch without example
204
- if not os.path.exists(config_path):
205
- with open(config_path, 'w') as f:
206
- json.dump(config, f, indent=4)
207
-
208
- return config_path
209
-
210
- @click.group(invoke_without_command=True)
211
- @click.option('--task', '-t', help='The task to automate')
212
- @click.option('--max-iter', '-m', default=10, help='Maximum number of iterations for the task')
213
- @click.option('--change-model', is_flag=True, help='Change the LLM provider and model')
214
- @click.pass_context
215
- def cli(ctx, task, max_iter, change_model):
216
- """TaskAutomator - Your AI Task Automation Tool"""
217
- # Ensure config exists and has required fields
218
- config_path = ensure_config_exists()
219
- clear_terminal()
220
-
221
- # If change-model flag is set, update the model
222
- if change_model:
223
- with open(config_path, 'r') as f:
224
- config = json.load(f)
225
-
226
- print("Current configuration: Provider: {}, Model: {}".format(config.get("llm_provider"), config.get("model_name")))
227
- questions = [
228
- inquirer.List('provider_key',
229
- message="Select LLM Provider",
230
- choices=list(AVAILABLE_MODELS.keys()) # User selects "openai", "mistral", etc.
231
- )
232
- ]
233
- selected_provider_key = inquirer.prompt(questions)['provider_key']
234
- clear_terminal()
235
-
236
- # Ensure API key exists for the selected provider
237
- ensure_api_key(selected_provider_key)
238
-
239
- questions = [
240
- inquirer.List('model_name_full',
241
- message=f"Select {selected_provider_key} Model",
242
- choices=AVAILABLE_MODELS[selected_provider_key] # Shows "openai/gpt-3.5-turbo", etc.
243
- )
244
- ]
245
- selected_model_name_full = inquirer.prompt(questions)['model_name_full']
246
- clear_terminal()
247
-
248
- config["llm_provider"] = selected_provider_key # Store "openai"
249
- config["model_name"] = selected_model_name_full # Store "openai/gpt-4o"
250
-
251
- # Ensure working_directory is preserved or set
252
- if "working_directory" not in config or not config["working_directory"]:
253
- config["working_directory"] = os.getcwd()
254
-
255
- with open(config_path, 'w') as f:
256
- json.dump(config, f, indent=4)
257
-
258
- click.echo(f"Model changed to {selected_model_name_full}")
259
- return
260
-
261
- # Ensure API key for the configured model before running OpenCopilot
262
- # This is a bit redundant if ensure_config_exists already did it, but good for safety
263
- with open(config_path, 'r') as f:
264
- config = json.load(f)
265
-
266
- current_provider = config.get("llm_provider")
267
- if not current_provider and config.get("model_name"): # If llm_provider is missing, try to derive it
268
- current_provider = get_provider_from_model_name(config["model_name"])
269
-
270
- if current_provider:
271
- ensure_api_key(current_provider)
272
- else:
273
- click.echo("Error: LLM provider not configured. Please run with --change-model to set it up.", err=True)
274
- return
275
-
276
- copilot = OpenCopilot()
277
- if ctx.invoked_subcommand is None:
278
- if task:
279
- copilot.run_task(user_prompt=task, max_iter=max_iter)
280
- else:
281
- copilot.run()
282
-
283
- @cli.command('list-tools')
284
- def list_tools():
285
- """List all available automation tools"""
286
- tools = OpenCopilot.list_available_tools()
287
- click.echo("Available Tools:")
288
- for tool in tools:
289
- click.echo(f"- {tool['name']}: {tool['summary']}")
290
- if tool.get('arguments'):
291
- click.echo(f" Arguments: {tool['arguments']}")
292
-
293
- @cli.command('list-models')
294
- def list_models():
295
- """List all available LLM providers and their models (litellm compatible)"""
296
- click.echo("Available LLM Providers and Models (litellm compatible):")
297
- for provider_key, model_list in AVAILABLE_MODELS.items():
298
- click.echo(f"\n{provider_key.upper()}:") # provider_key is "openai", "mistral", etc.
299
- for model_name_full in model_list: # model_name_full is "openai/gpt-4o", etc.
300
- click.echo(f" - {model_name_full}")
301
-
302
- @cli.command('set-api-key')
303
- @click.option('--provider', '-p', type=click.Choice(list(AVAILABLE_MODELS.keys())),
304
- help='The LLM provider to set API key for')
305
- @click.option('--key', '-k', help='The API key to set (if not provided, will prompt for it)')
306
- def set_api_key(provider, key):
307
- """Set or update API key for a specific LLM provider"""
308
- if not provider:
309
- # If no provider specified, ask user to choose
310
- questions = [
311
- inquirer.List('provider_key',
312
- message="Select LLM Provider to update API key",
313
- choices=list(AVAILABLE_MODELS.keys())
314
- )
315
- ]
316
- provider = inquirer.prompt(questions)['provider_key']
317
-
318
- # Get the environment variable name for this provider
319
- env_var = API_KEYS.get(provider)
320
- if not env_var:
321
- raise ValueError(f"Unknown provider: {provider}")
322
-
323
- # Get the API key (either from command line or prompt)
324
- if not key:
325
- questions = [
326
- inquirer.Text('api_key',
327
- message=f"Enter your {provider.upper()} API key",
328
- validate=lambda _, x: len(x.strip()) > 0
329
- )
330
- ]
331
- key = inquirer.prompt(questions)['api_key']
332
-
333
- # Get the path to .env file
334
- env_path = os.path.join(os.path.dirname(__file__), '../.env')
335
-
336
- # Read existing .env file if it exists
337
- lines = []
338
- if os.path.exists(env_path):
339
- with open(env_path, 'r') as f:
340
- lines = f.readlines()
341
-
342
- # Update or add the API key
343
- key_line = f"{env_var}={key}\n"
344
- key_exists = False
345
-
346
- for i, line in enumerate(lines):
347
- if line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}="):
348
- lines[i] = key_line
349
- key_exists = True
350
- break
351
-
352
- if not key_exists:
353
- lines.append(key_line)
354
-
355
- # Write back to .env file
356
- with open(env_path, 'w') as f:
357
- f.writelines(lines)
358
-
359
- click.echo(f"API key for {provider.upper()} has been updated successfully in {env_path}")
360
-
361
- if __name__ == '__main__':
362
- cli()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes