PikoAi 0.1.16__tar.gz → 0.1.18__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.
- {pikoai-0.1.16 → pikoai-0.1.18}/PKG-INFO +1 -1
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Agents/Executor/executor.py +3 -18
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Agents/Executor/prompts.py +5 -7
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/PKG-INFO +1 -1
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/tool_manager.py +1 -1
- pikoai-0.1.18/Src/Tools/web_search.py +58 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Utils/ter_interface.py +2 -1
- pikoai-0.1.18/Src/cli.py +300 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/llm_interface/llm.py +28 -2
- {pikoai-0.1.16 → pikoai-0.1.18}/setup.py +1 -1
- pikoai-0.1.16/Src/Tools/web_search.py +0 -30
- pikoai-0.1.16/Src/cli.py +0 -362
- {pikoai-0.1.16 → pikoai-0.1.18}/LICENSE +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/README.md +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Agents/Executor/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Agents/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/base_env.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/base_executor.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/env.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/js_executor.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/python_executor.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/shell.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/tests/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/tests/test_python_executor.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Env/tests/test_shell_executor.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/OpenCopilot.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/SOURCES.txt +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/dependency_links.txt +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/entry_points.txt +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/requires.txt +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/PikoAi.egg-info/top_level.txt +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/file_task.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/system_details.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/tool_dir.json +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/userinp.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Tools/web_loader.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Utils/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/Utils/executor_utils.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/Src/llm_interface/__init__.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/setup.cfg +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/test/test.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/test/test_file_task.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/test/test_opencopilot_file_integration.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/test/testjs.py +0 -0
- {pikoai-0.1.16 → pikoai-0.1.18}/test/testscript.py +0 -0
@@ -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
|
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
|
-
from llm_interface.llm import LiteLLMInterface # Import LiteLLMInterface
|
21
|
-
|
12
|
+
from llm_interface.llm import LiteLLMInterface # Import LiteLLMInterfacea
|
22
13
|
from Tools import tool_manager
|
23
14
|
|
24
15
|
class RateLimiter:
|
@@ -38,7 +29,7 @@ class executor:
|
|
38
29
|
def __init__(self, user_prompt, max_iter=10):
|
39
30
|
self.user_prompt = user_prompt
|
40
31
|
self.max_iter = max_iter
|
41
|
-
self.rate_limiter = RateLimiter(wait_time=
|
32
|
+
self.rate_limiter = RateLimiter(wait_time=3.0, max_retries=3)
|
42
33
|
self.executor_prompt_init() # Update system_prompt
|
43
34
|
# self.python_executor = python_executor.PythonExecutor() # Initialize PythonExecutor
|
44
35
|
# self.shell_executor = ShellExecutor() # Initialize ShellExecutor
|
@@ -89,16 +80,10 @@ class executor:
|
|
89
80
|
|
90
81
|
except Exception as e: # Catching generic Exception as LiteLLM maps to OpenAI exceptions
|
91
82
|
# Check if the error message contains "429" for rate limiting
|
92
|
-
if
|
83
|
+
if retries < self.rate_limiter.max_retries:
|
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
|
@@ -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
|
-
- **
|
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
|
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
|
-
-
|
54
|
-
- Always evaluate the output of
|
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
|
54
|
+
- Do not end the task immediately after a tool call without evaluating its output.
|
57
55
|
|
58
56
|
"""
|
@@ -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
|
-
|
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,
|
@@ -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}")
|
@@ -68,7 +68,8 @@ class TerminalInterface:
|
|
68
68
|
|
69
69
|
# Handle tool call closing delimiter - be more flexible with whitespace
|
70
70
|
elif "<<END_TOOL_CALL>>" in line_stripped:
|
71
|
-
self.console.print(
|
71
|
+
self.console.print(self.tool_call_buffer)
|
72
|
+
# self.console.print(Syntax('{"status": "end_tool_call"}', "json", theme="monokai", line_numbers=False))
|
72
73
|
self.console.print("[bold cyan]--------------------------------[/bold cyan]")
|
73
74
|
self.inside_tool_call = False
|
74
75
|
self.tool_call_buffer = ""
|
pikoai-0.1.18/Src/cli.py
ADDED
@@ -0,0 +1,300 @@
|
|
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
|
+
"gemini": [
|
37
|
+
"gemini/gemini-2.0-flash",
|
38
|
+
"gemini/gemini-2.5-flash-preview-05-20"
|
39
|
+
]
|
40
|
+
}
|
41
|
+
|
42
|
+
# Define API key environment variables for each provider (matching litellm conventions)
|
43
|
+
API_KEYS = {
|
44
|
+
"openai": "OPENAI_API_KEY",
|
45
|
+
"mistral": "MISTRAL_API_KEY",
|
46
|
+
"groq": "GROQ_API_KEY",
|
47
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
48
|
+
"gemini": "GEMINI_API_KEY"
|
49
|
+
}
|
50
|
+
|
51
|
+
# --- Utility Functions ---
|
52
|
+
|
53
|
+
def clear_terminal():
|
54
|
+
"""Clear the terminal screen."""
|
55
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
56
|
+
|
57
|
+
def get_provider_from_model_name(model_name: str) -> str:
|
58
|
+
"""Extracts the provider from a litellm model string (e.g., 'openai/gpt-4o' -> 'openai')."""
|
59
|
+
if not model_name or '/' not in model_name:
|
60
|
+
print(f"Warning: Model name '{model_name}' may not be in 'provider/model' format. Attempting to use as provider.")
|
61
|
+
return model_name
|
62
|
+
return model_name.split('/')[0]
|
63
|
+
|
64
|
+
# --- Configuration Management ---
|
65
|
+
|
66
|
+
def load_config(config_path: str) -> dict:
|
67
|
+
"""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()."""
|
68
|
+
if not os.path.exists(config_path):
|
69
|
+
example_path = os.path.join(os.path.dirname(__file__), '../config.example.json')
|
70
|
+
if os.path.exists(example_path):
|
71
|
+
shutil.copy2(example_path, config_path)
|
72
|
+
with open(config_path, 'r') as f:
|
73
|
+
config = json.load(f)
|
74
|
+
else:
|
75
|
+
config = { "working_directory": os.getcwd(), "llm_provider": None, "model_name": None }
|
76
|
+
else:
|
77
|
+
with open(config_path, 'r') as f:
|
78
|
+
try:
|
79
|
+
config = json.load(f)
|
80
|
+
except json.JSONDecodeError:
|
81
|
+
print("Error reading config.json. File might be corrupted. Re-creating default.")
|
82
|
+
config = { "working_directory": os.getcwd(), "llm_provider": None, "model_name": None }
|
83
|
+
# Always update working_directory to current directory
|
84
|
+
config["working_directory"] = os.getcwd()
|
85
|
+
return config
|
86
|
+
|
87
|
+
def save_config(config_path: str, config: dict) -> None:
|
88
|
+
"""Save config dict (with updated working_directory) to config_path."""
|
89
|
+
with open(config_path, 'w') as f:
|
90
|
+
json.dump(config, f, indent=4)
|
91
|
+
|
92
|
+
# --- API Key Management ---
|
93
|
+
|
94
|
+
def ensure_api_key(provider: str):
|
95
|
+
"""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."""
|
96
|
+
env_path = os.path.join(os.path.dirname(__file__), '../.env')
|
97
|
+
env_var = API_KEYS.get(provider)
|
98
|
+
if not env_var:
|
99
|
+
raise ValueError(f"Unknown provider: {provider}")
|
100
|
+
|
101
|
+
# Force reload .env (if it exists) so that any new key is picked up.
|
102
|
+
if os.path.exists(env_path):
|
103
|
+
load_dotenv(env_path, override=True)
|
104
|
+
|
105
|
+
api_key = os.getenv(env_var)
|
106
|
+
if not api_key:
|
107
|
+
questions = [ inquirer.Text("api_key", message=f"Enter your {provider.upper()} API key", validate=lambda _, x: len(x.strip()) > 0) ]
|
108
|
+
api_key = inquirer.prompt(questions)["api_key"]
|
109
|
+
clear_terminal()
|
110
|
+
# Save (or update) the key in .env
|
111
|
+
lines = []
|
112
|
+
if os.path.exists(env_path):
|
113
|
+
with open(env_path, 'r') as f:
|
114
|
+
lines = f.readlines()
|
115
|
+
key_line = f"{env_var}={api_key}\n"
|
116
|
+
key_exists = False
|
117
|
+
for (i, line) in enumerate(lines):
|
118
|
+
if (line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}=")):
|
119
|
+
lines[i] = key_line
|
120
|
+
key_exists = True
|
121
|
+
break
|
122
|
+
if not key_exists:
|
123
|
+
lines.append(key_line)
|
124
|
+
with open(env_path, 'w') as f:
|
125
|
+
f.writelines(lines)
|
126
|
+
# Reload .env (override) so that the new key is available.
|
127
|
+
load_dotenv(env_path, override=True)
|
128
|
+
|
129
|
+
|
130
|
+
# --- Model / Provider Management ---
|
131
|
+
|
132
|
+
def prompt_model_selection() -> tuple:
|
133
|
+
"""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)."""
|
134
|
+
questions = [ inquirer.List("provider_key", message="Select LLM Provider", choices=list(AVAILABLE_MODELS.keys())) ]
|
135
|
+
selected_provider_key = inquirer.prompt(questions)["provider_key"]
|
136
|
+
clear_terminal()
|
137
|
+
# (Ensure API key for the selected provider.)
|
138
|
+
ensure_api_key(selected_provider_key)
|
139
|
+
questions = [ inquirer.List("model_name_full", message=f"Select {selected_provider_key} Model", choices=AVAILABLE_MODELS[selected_provider_key]) ]
|
140
|
+
selected_model_name_full = inquirer.prompt(questions)["model_name_full"]
|
141
|
+
clear_terminal()
|
142
|
+
return (selected_provider_key, selected_model_name_full)
|
143
|
+
|
144
|
+
def update_model_config(config_path: str, provider_key: str, model_name_full: str) -> None:
|
145
|
+
"""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.)"""
|
146
|
+
config = load_config(config_path)
|
147
|
+
config["llm_provider"] = provider_key
|
148
|
+
config["model_name"] = model_name_full
|
149
|
+
if "working_directory" not in config or not config["working_directory"]:
|
150
|
+
config["working_directory"] = os.getcwd()
|
151
|
+
save_config(config_path, config)
|
152
|
+
|
153
|
+
# --- CLI Commands ---
|
154
|
+
|
155
|
+
@click.group(invoke_without_command=True, help="TaskAutomator – Your AI Task Automation Tool\n\nThis tool helps automate tasks using AI. You can run tasks directly or use various commands to manage settings and tools.")
|
156
|
+
@click.option("--task", "-t", help="The task to automate (e.g., 'create a python script that sorts files by date')")
|
157
|
+
@click.option("--max-iter", "-m", default=10, help="Maximum number of iterations for the task (default: 10)")
|
158
|
+
@click.option("--change-model", is_flag=True, help="Change the LLM provider and model before running the task")
|
159
|
+
@click.pass_context
|
160
|
+
def cli(ctx, task, max_iter, change_model):
|
161
|
+
"""TaskAutomator – Your AI Task Automation Tool
|
162
|
+
|
163
|
+
This tool helps automate tasks using AI. You can:
|
164
|
+
- Run tasks directly with --task
|
165
|
+
- Change AI models with --change-model
|
166
|
+
- Manage API keys with set-api-key and set-serp-key
|
167
|
+
- List available tools and models
|
168
|
+
"""
|
169
|
+
config_path = os.path.join(os.path.dirname(__file__), '../config.json')
|
170
|
+
config = load_config(config_path)
|
171
|
+
save_config(config_path, config)
|
172
|
+
|
173
|
+
clear_terminal()
|
174
|
+
|
175
|
+
if change_model or not config.get("model_name"):
|
176
|
+
(provider, model) = prompt_model_selection()
|
177
|
+
update_model_config(config_path, provider, model)
|
178
|
+
click.echo(f"Model changed to {model}")
|
179
|
+
if not change_model: # Only return if this was triggered by missing model
|
180
|
+
return
|
181
|
+
|
182
|
+
# Ensure API key for the configured model (or derive provider from model_name if missing) before running OpenCopilot.
|
183
|
+
current_provider = config.get("llm_provider")
|
184
|
+
if not current_provider and config.get("model_name"):
|
185
|
+
current_provider = get_provider_from_model_name(config["model_name"])
|
186
|
+
if current_provider:
|
187
|
+
ensure_api_key(current_provider)
|
188
|
+
else:
|
189
|
+
click.echo("Error: LLM provider not configured. Please run with --change-model to set it up.", err=True)
|
190
|
+
return
|
191
|
+
|
192
|
+
copilot = OpenCopilot()
|
193
|
+
if ctx.invoked_subcommand is None:
|
194
|
+
if task:
|
195
|
+
copilot.run_task(user_prompt=task, max_iter=max_iter)
|
196
|
+
else:
|
197
|
+
copilot.run()
|
198
|
+
|
199
|
+
@cli.command("list-tools", help="List all available automation tools and their descriptions")
|
200
|
+
def list_tools():
|
201
|
+
"""List all available automation tools and their descriptions.
|
202
|
+
|
203
|
+
This command shows all tools that can be used by the AI to automate tasks,
|
204
|
+
including what each tool does and what arguments it accepts.
|
205
|
+
"""
|
206
|
+
tools = OpenCopilot.list_available_tools()
|
207
|
+
click.echo("Available Tools:")
|
208
|
+
for tool in tools:
|
209
|
+
click.echo(f"- {tool['name']}: {tool['summary']}")
|
210
|
+
if tool.get("arguments"):
|
211
|
+
click.echo(f" Arguments: {tool['arguments']}")
|
212
|
+
|
213
|
+
@cli.command("list-models", help="List all available LLM providers and their models")
|
214
|
+
def list_models():
|
215
|
+
"""List all available LLM providers and their models.
|
216
|
+
|
217
|
+
Shows all supported AI models that can be used for task automation,
|
218
|
+
organized by provider (OpenAI, Mistral, Groq, Anthropic).
|
219
|
+
"""
|
220
|
+
click.echo("Available LLM Providers and Models (litellm compatible):")
|
221
|
+
for (provider_key, model_list) in AVAILABLE_MODELS.items():
|
222
|
+
click.echo(f"\n{provider_key.upper()}:")
|
223
|
+
for model_name_full in model_list:
|
224
|
+
click.echo(f" - {model_name_full}")
|
225
|
+
|
226
|
+
@cli.command("set-api-key", help="Set or update API key for an LLM provider")
|
227
|
+
@click.option("--provider", "-p", type=click.Choice(list(AVAILABLE_MODELS.keys())), help="The LLM provider to set API key for (e.g., openai, mistral, groq, anthropic)")
|
228
|
+
@click.option("--key", "-k", help="The API key to set (if not provided, will prompt for it securely)")
|
229
|
+
def set_api_key(provider, key):
|
230
|
+
"""Set or update API key for an LLM provider.
|
231
|
+
|
232
|
+
This command allows you to set or update the API key for any supported LLM provider.
|
233
|
+
The key will be stored securely in your .env file.
|
234
|
+
|
235
|
+
Examples:
|
236
|
+
piko set-api-key --provider openai
|
237
|
+
piko set-api-key -p mistral -k your-key-here
|
238
|
+
"""
|
239
|
+
if not provider:
|
240
|
+
questions = [ inquirer.List("provider_key", message="Select LLM Provider to update API key", choices=list(AVAILABLE_MODELS.keys())) ]
|
241
|
+
provider = inquirer.prompt(questions)["provider_key"]
|
242
|
+
env_var = API_KEYS.get(provider)
|
243
|
+
if not env_var:
|
244
|
+
raise ValueError(f"Unknown provider: {provider}")
|
245
|
+
if not key:
|
246
|
+
questions = [ inquirer.Text("api_key", message=f"Enter your {provider.upper()} API key", validate=lambda _, x: len(x.strip()) > 0) ]
|
247
|
+
key = inquirer.prompt(questions)["api_key"]
|
248
|
+
env_path = os.path.join(os.path.dirname(__file__), '../.env')
|
249
|
+
lines = []
|
250
|
+
if os.path.exists(env_path):
|
251
|
+
with open(env_path, 'r') as f:
|
252
|
+
lines = f.readlines()
|
253
|
+
key_line = f"{env_var}={key}\n"
|
254
|
+
key_exists = False
|
255
|
+
for (i, line) in enumerate(lines):
|
256
|
+
if (line.strip().startswith(f"{env_var}=") or line.strip().startswith(f"#{env_var}=")):
|
257
|
+
lines[i] = key_line
|
258
|
+
key_exists = True
|
259
|
+
break
|
260
|
+
if not key_exists:
|
261
|
+
lines.append(key_line)
|
262
|
+
with open(env_path, 'w') as f:
|
263
|
+
f.writelines(lines)
|
264
|
+
click.echo(f"API key for {provider.upper()} has been updated successfully in {env_path}")
|
265
|
+
|
266
|
+
@cli.command("set-serp-key", help="Set or update the SERP API key for web search functionality")
|
267
|
+
@click.option("--key", "-k", help="The SERP API key to set (if not provided, will prompt for it securely)")
|
268
|
+
def set_serp_key(key):
|
269
|
+
"""Set or update the SERP API key used for web search functionality.
|
270
|
+
|
271
|
+
This command sets the API key used for web search operations when DuckDuckGo
|
272
|
+
search is not available. The key will be stored securely in your .env file.
|
273
|
+
|
274
|
+
Examples:
|
275
|
+
piko set-serp-key
|
276
|
+
piko set-serp-key -k your-key-here
|
277
|
+
"""
|
278
|
+
if not key:
|
279
|
+
questions = [ inquirer.Text("api_key", message="Enter your SERP API key", validate=lambda _, x: len(x.strip()) > 0) ]
|
280
|
+
key = inquirer.prompt(questions)["api_key"]
|
281
|
+
env_path = os.path.join(os.path.dirname(__file__), '../.env')
|
282
|
+
lines = []
|
283
|
+
if os.path.exists(env_path):
|
284
|
+
with open(env_path, 'r') as f:
|
285
|
+
lines = f.readlines()
|
286
|
+
key_line = f"SERP_API_KEY={key}\n"
|
287
|
+
key_exists = False
|
288
|
+
for (i, line) in enumerate(lines):
|
289
|
+
if (line.strip().startswith("SERP_API_KEY=") or line.strip().startswith("#SERP_API_KEY=")):
|
290
|
+
lines[i] = key_line
|
291
|
+
key_exists = True
|
292
|
+
break
|
293
|
+
if not key_exists:
|
294
|
+
lines.append(key_line)
|
295
|
+
with open(env_path, 'w') as f:
|
296
|
+
f.writelines(lines)
|
297
|
+
click.echo(f"SERP API key has been updated successfully in {env_path}")
|
298
|
+
|
299
|
+
if __name__ == '__main__':
|
300
|
+
cli()
|
@@ -7,11 +7,26 @@ import os
|
|
7
7
|
import sys
|
8
8
|
import json
|
9
9
|
import litellm # Added import for litellm
|
10
|
+
import logging
|
11
|
+
from datetime import datetime
|
10
12
|
|
11
13
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
12
14
|
|
13
15
|
from Utils.ter_interface import TerminalInterface
|
14
16
|
|
17
|
+
# Set up logging
|
18
|
+
log_dir = os.path.join(os.path.dirname(__file__), '../../logs')
|
19
|
+
os.makedirs(log_dir, exist_ok=True)
|
20
|
+
log_file = os.path.join(log_dir, 'llm_responses.log')
|
21
|
+
|
22
|
+
# Configure logging
|
23
|
+
logging.basicConfig(
|
24
|
+
level=logging.INFO,
|
25
|
+
format='%(asctime)s - %(message)s',
|
26
|
+
handlers=[
|
27
|
+
logging.FileHandler(log_file) # Only log to file, removed StreamHandler
|
28
|
+
]
|
29
|
+
)
|
15
30
|
|
16
31
|
# Load environment variables from .env file
|
17
32
|
load_dotenv()
|
@@ -30,6 +45,15 @@ class LiteLLMInterface:
|
|
30
45
|
def __init__(self):
|
31
46
|
self.terminal = TerminalInterface()
|
32
47
|
self.model_name = self.load_config()
|
48
|
+
logging.info(f"\n{'='*50}\nNew Session - Using model: {self.model_name}\n{'='*50}")
|
49
|
+
|
50
|
+
def log_response(self, response_content):
|
51
|
+
"""Log only the LLM response in a readable format."""
|
52
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
53
|
+
log_entry = f"\n{'='*50}\nTimestamp: {timestamp}\nModel: {self.model_name}\n\n"
|
54
|
+
log_entry += f"Response:\n{response_content}\n"
|
55
|
+
log_entry += f"{'='*50}\n"
|
56
|
+
logging.info(log_entry)
|
33
57
|
|
34
58
|
def load_config(self):
|
35
59
|
config_path = os.path.join(os.path.dirname(__file__), '../../config.json')
|
@@ -60,10 +84,12 @@ class LiteLLMInterface:
|
|
60
84
|
response_content += content
|
61
85
|
|
62
86
|
self.terminal.flush_markdown()
|
87
|
+
# Log only the response after successful completion
|
88
|
+
self.log_response(response_content)
|
63
89
|
return response_content
|
64
90
|
except Exception as e:
|
65
|
-
#
|
66
|
-
|
91
|
+
# Log the error
|
92
|
+
logging.error(f"\n{'='*50}\nError occurred:\nModel: {self.model_name}\nError: {str(e)}\n{'='*50}")
|
67
93
|
print(f"An error occurred during the API call: {e}")
|
68
94
|
self.terminal.flush_markdown() # Ensure terminal is flushed even on error
|
69
95
|
raise # Re-raise the exception to be caught by the executor
|
@@ -1,30 +0,0 @@
|
|
1
|
-
from duckduckgo_search import DDGS
|
2
|
-
|
3
|
-
def web_search(max_results: int = 10, **kwargs) -> str:
|
4
|
-
"""
|
5
|
-
Performs a DuckDuckGo web search based on your query (think a Google search) then returns the top search results.
|
6
|
-
|
7
|
-
Args:
|
8
|
-
query (str): The search query to perform.
|
9
|
-
max_results (int, optional): Maximum number of results to return. Defaults to 10.
|
10
|
-
**kwargs: Additional keyword arguments to pass to DDGS.
|
11
|
-
|
12
|
-
Returns:
|
13
|
-
str: Formatted string containing search results.
|
14
|
-
|
15
|
-
Raises:
|
16
|
-
ImportError: If the duckduckgo_search package is not installed.
|
17
|
-
Exception: If no results are found for the given query.
|
18
|
-
"""
|
19
|
-
try:
|
20
|
-
ddgs = DDGS()
|
21
|
-
except ImportError as e:
|
22
|
-
raise ImportError("You must install package `duckduckgo_search` to run this function: for instance run `pip install duckduckgo-search`."
|
23
|
-
) from e
|
24
|
-
query = kwargs['query']
|
25
|
-
results = ddgs.text(query, max_results=max_results)
|
26
|
-
if len(results) == 0:
|
27
|
-
raise Exception("No results found! Try a less restrictive/shorter query.")
|
28
|
-
|
29
|
-
postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
|
30
|
-
return "## Search Results\n\n" + "\n\n".join(postprocessed_results)
|
pikoai-0.1.16/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
|
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
|
File without changes
|