PikoAi 0.1.16__py3-none-any.whl → 0.1.18__py3-none-any.whl

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.
@@ -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
- 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=5.0, max_retries=3)
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 "429" in str(e) and retries < self.rate_limiter.max_retries:
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
- - **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
  """
Tools/tool_manager.py CHANGED
@@ -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,
Tools/web_search.py CHANGED
@@ -1,8 +1,14 @@
1
+ import os
2
+ from dotenv import load_dotenv
1
3
  from duckduckgo_search import DDGS
4
+ import requests
5
+
6
+ load_dotenv()
2
7
 
3
8
  def web_search(max_results: int = 10, **kwargs) -> str:
4
9
  """
5
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.
6
12
 
7
13
  Args:
8
14
  query (str): The search query to perform.
@@ -13,18 +19,40 @@ def web_search(max_results: int = 10, **kwargs) -> str:
13
19
  str: Formatted string containing search results.
14
20
 
15
21
  Raises:
16
- ImportError: If the duckduckgo_search package is not installed.
22
+ ImportError: If neither duckduckgo_search nor SerpAPI is available.
17
23
  Exception: If no results are found for the given query.
18
24
  """
25
+ query = kwargs['query']
26
+ # Try DuckDuckGo first
19
27
  try:
20
28
  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.")
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
28
35
 
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)
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}")
Utils/ter_interface.py CHANGED
@@ -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(Syntax('{"status": "end_tool_call"}', "json", theme="monokai", line_numbers=False))
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 = ""
cli.py CHANGED
@@ -32,6 +32,10 @@ AVAILABLE_MODELS = {
32
32
  "anthropic/claude-3-opus-20240229",
33
33
  "anthropic/claude-3-sonnet-20240229",
34
34
  "anthropic/claude-3-haiku-20240307"
35
+ ],
36
+ "gemini": [
37
+ "gemini/gemini-2.0-flash",
38
+ "gemini/gemini-2.5-flash-preview-05-20"
35
39
  ]
36
40
  }
37
41
 
@@ -40,323 +44,257 @@ API_KEYS = {
40
44
  "openai": "OPENAI_API_KEY",
41
45
  "mistral": "MISTRAL_API_KEY",
42
46
  "groq": "GROQ_API_KEY",
43
- "anthropic": "ANTHROPIC_API_KEY"
47
+ "anthropic": "ANTHROPIC_API_KEY",
48
+ "gemini": "GEMINI_API_KEY"
44
49
  }
45
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
+
46
57
  def get_provider_from_model_name(model_name: str) -> str:
47
58
  """Extracts the provider from a litellm model string (e.g., 'openai/gpt-4o' -> 'openai')."""
48
59
  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
60
  print(f"Warning: Model name '{model_name}' may not be in 'provider/model' format. Attempting to use as provider.")
54
61
  return model_name
55
62
  return model_name.split('/')[0]
56
63
 
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
64
+ # --- Configuration Management ---
115
65
 
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
-
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()."""
121
68
  if not os.path.exists(config_path):
122
- # Copy config from example if it doesn't exist
123
69
  example_path = os.path.join(os.path.dirname(__file__), '../config.example.json')
124
70
  if os.path.exists(example_path):
125
71
  shutil.copy2(example_path, config_path)
126
72
  with open(config_path, 'r') as f:
127
73
  config = json.load(f)
128
74
  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
- }
75
+ config = { "working_directory": os.getcwd(), "llm_provider": None, "model_name": None }
135
76
  else:
136
- # Read existing config
137
77
  with open(config_path, 'r') as f:
138
78
  try:
139
79
  config = json.load(f)
140
80
  except json.JSONDecodeError:
141
81
  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
- }
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
147
86
 
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()
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)
151
91
 
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']
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"]
174
109
  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}'")
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
+
182
129
 
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)
130
+ # --- Model / Provider Management ---
195
131
 
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.")
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)
201
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)
202
152
 
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
153
+ # --- CLI Commands ---
209
154
 
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')
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")
214
159
  @click.pass_context
215
160
  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
161
+ """TaskAutomator Your AI Task Automation Tool
260
162
 
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)
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)
265
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.
266
183
  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
-
184
+ if not current_provider and config.get("model_name"):
185
+ current_provider = get_provider_from_model_name(config["model_name"])
270
186
  if current_provider:
271
- ensure_api_key(current_provider)
187
+ ensure_api_key(current_provider)
272
188
  else:
273
- click.echo("Error: LLM provider not configured. Please run with --change-model to set it up.", err=True)
274
- return
189
+ click.echo("Error: LLM provider not configured. Please run with --change-model to set it up.", err=True)
190
+ return
275
191
 
276
192
  copilot = OpenCopilot()
277
193
  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()
194
+ if task:
195
+ copilot.run_task(user_prompt=task, max_iter=max_iter)
196
+ else:
197
+ copilot.run()
282
198
 
283
- @cli.command('list-tools')
199
+ @cli.command("list-tools", help="List all available automation tools and their descriptions")
284
200
  def list_tools():
285
- """List all available automation 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
+ """
286
206
  tools = OpenCopilot.list_available_tools()
287
207
  click.echo("Available Tools:")
288
208
  for tool in tools:
289
- click.echo(f"- {tool['name']}: {tool['summary']}")
290
- if tool.get('arguments'):
291
- click.echo(f" Arguments: {tool['arguments']}")
209
+ click.echo(f"- {tool['name']}: {tool['summary']}")
210
+ if tool.get("arguments"):
211
+ click.echo(f" Arguments: {tool['arguments']}")
292
212
 
293
- @cli.command('list-models')
213
+ @cli.command("list-models", help="List all available LLM providers and their models")
294
214
  def list_models():
295
- """List all available LLM providers and their models (litellm compatible)"""
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
+ """
296
220
  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}")
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}")
301
225
 
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)')
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)")
306
229
  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']
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.
317
234
 
318
- # Get the environment variable name for this provider
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"]
319
242
  env_var = API_KEYS.get(provider)
320
243
  if not env_var:
321
- raise ValueError(f"Unknown provider: {provider}")
322
-
323
- # Get the API key (either from command line or prompt)
244
+ raise ValueError(f"Unknown provider: {provider}")
324
245
  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
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"]
334
248
  env_path = os.path.join(os.path.dirname(__file__), '../.env')
335
-
336
- # Read existing .env file if it exists
337
249
  lines = []
338
250
  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
251
+ with open(env_path, 'r') as f:
252
+ lines = f.readlines()
343
253
  key_line = f"{env_var}={key}\n"
344
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.
345
270
 
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
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.
351
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
352
293
  if not key_exists:
353
- lines.append(key_line)
354
-
355
- # Write back to .env file
294
+ lines.append(key_line)
356
295
  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}")
296
+ f.writelines(lines)
297
+ click.echo(f"SERP API key has been updated successfully in {env_path}")
360
298
 
361
299
  if __name__ == '__main__':
362
300
  cli()
llm_interface/llm.py CHANGED
@@ -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
- # litellm maps exceptions to OpenAI exceptions.
66
- # The executor should catch these and handle them.
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PikoAi
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: An AI-powered task automation tool
5
5
  Home-page: https://github.com/nihaaaar22/OS-Assistant
6
6
  Author: Nihar S
@@ -1,9 +1,9 @@
1
1
  OpenCopilot.py,sha256=kPTs0-ly84h4dM7AmBlK4uwst5Sj2AM6UAlE3okkD8U,12157
2
- cli.py,sha256=qDWlKyOUkhDHSBxQ2wjy1e6KGRsDxE7pjxUZKjQFC3k,13959
2
+ cli.py,sha256=2UvmH74pcBFFezI0WHNyWTHMYasIM5NGnrUX6wsdveM,12945
3
3
  Agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  Agents/Executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- Agents/Executor/executor.py,sha256=OAqrDdjO62Gy4UKEVF0yYYJjkvVJo_mQy_OMJSibl58,8255
6
- Agents/Executor/prompts.py,sha256=dWmUiAloNN_NKwWgaG_IqHDhCX1O-VhwbadUL-16gZw,2903
5
+ Agents/Executor/executor.py,sha256=BzSYkT4aPW1yDLSNXNr9WEWZEcs1becEYYlop-eB8s8,6999
6
+ Agents/Executor/prompts.py,sha256=pGY4uXNGYiw_TnTUsRjrVsWc9CV657q3916eui0oulU,2688
7
7
  Env/__init__.py,sha256=KLe7UcNV5L395SxhMwbYGyu7KPrSNaoV_9QJo3mLop0,196
8
8
  Env/base_env.py,sha256=K4PoWwPXn3pKeu7_-JOlUuyNbyYQ9itMhQybFOm-3K4,1563
9
9
  Env/base_executor.py,sha256=awTwJ44CKWV4JO2KUHfHDX0p1Ujw55hlaL5oNYTEW9M,893
@@ -18,18 +18,18 @@ Tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  Tools/file_task.py,sha256=VUhWq_G-SWvGahQo8PG7TOpElHUW3BGLUabrTdJS89o,12151
19
19
  Tools/system_details.py,sha256=RScVnhTpUOlNG0g5bGnwmtNr5nSJzOec8HJSFpbicds,2651
20
20
  Tools/tool_dir.json,sha256=RTawcanxIkJaUys6Y3yftXAT5uxMH0xPZYTtD1ilJl0,3119
21
- Tools/tool_manager.py,sha256=86qwREw5an12dweIGCS1NNgINwHikyTxUpbjPWoLbt0,4118
21
+ Tools/tool_manager.py,sha256=xrjrGmLI1hXqCfALBRBuMFCgXwc7pb5TpKMBH_4tCzs,4106
22
22
  Tools/userinp.py,sha256=SK69fMEdUvNQor9V3BVckeDMJcq71g6H6EHPmNfsZD4,834
23
23
  Tools/web_loader.py,sha256=_oP48uwveTaCKU7G5ju2zsJGTcZd1ScXTKOvHDFtZJU,4564
24
- Tools/web_search.py,sha256=4EGq1VZqfDgG-_yXTd4_Ha1iEUcR-szdlgRV7oFPru4,1259
24
+ Tools/web_search.py,sha256=12_VhwJGXmn3oUNhTbQ5ENFG964t9DWkfCz3UtlxrbM,2261
25
25
  Utils/__init__.py,sha256=oukU0ufroPRd8_N8d2xiFes9CTxSaw4NA6p2nS1kkSg,16
26
26
  Utils/executor_utils.py,sha256=WwK3TKgw_hG_crg7ijRaqfidYnnNXYbbs37vKZRYK-0,491
27
- Utils/ter_interface.py,sha256=2k32kVxcxVBewXnjSGSPbx25ZLhKindEMtL6wSC_wL4,3829
27
+ Utils/ter_interface.py,sha256=8Oe5818MAYC21SfUxtfnV9HQFcQ49z8Q030jjPqNP_g,3889
28
28
  llm_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- llm_interface/llm.py,sha256=tI_KDOW14QLWowA7bB3GPe2qjlk0sjS5fBavs9XD1fo,5185
30
- pikoai-0.1.16.dist-info/licenses/LICENSE,sha256=cELUVOboOAderKFp8bdtcM5VyJi61YH1oDbRhOuoQZw,1067
31
- pikoai-0.1.16.dist-info/METADATA,sha256=218K7ggeGVwRAv1lubC0_rATLNEpMWUVBP8eFYmenb0,2962
32
- pikoai-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- pikoai-0.1.16.dist-info/entry_points.txt,sha256=xjZnheDymNDnQ0o84R0jZKEITrhNbzQWN-AhqfA_d6s,50
34
- pikoai-0.1.16.dist-info/top_level.txt,sha256=hWzBNE7UQsuNcENIOksGcJED08k3ZGRRn2X5jnStICU,53
35
- pikoai-0.1.16.dist-info/RECORD,,
29
+ llm_interface/llm.py,sha256=TEKKgRfZGtk_UgBfqqkdWKHr4NS3jOOk9di0S3RA7c0,6216
30
+ pikoai-0.1.18.dist-info/licenses/LICENSE,sha256=cELUVOboOAderKFp8bdtcM5VyJi61YH1oDbRhOuoQZw,1067
31
+ pikoai-0.1.18.dist-info/METADATA,sha256=BM1IQxmZ4D7p07EfgLAuUGhpLvAT3W9oPw9WTV8Lee0,2962
32
+ pikoai-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ pikoai-0.1.18.dist-info/entry_points.txt,sha256=xjZnheDymNDnQ0o84R0jZKEITrhNbzQWN-AhqfA_d6s,50
34
+ pikoai-0.1.18.dist-info/top_level.txt,sha256=hWzBNE7UQsuNcENIOksGcJED08k3ZGRRn2X5jnStICU,53
35
+ pikoai-0.1.18.dist-info/RECORD,,