Skip to content

Commit 0f5ad51

Browse files
authored
Merge pull request #7 from ayusrjn/dev
New Features Interactive first-run setup prompts for Gemini API key and model, persisting to ~/.config/lambda-agent/config.env with optional per-project overrides. New top-level lambda CLI for invocation. Terminal spinner during model interactions and improved workspace-aware agent behavior. Documentation README updated with simplified pip install, first-time setup, and CLI usage. Chores Packaging added for easier installation and distribution.
2 parents 46a999f + 9608f26 commit 0f5ad51

12 files changed

Lines changed: 386 additions & 121 deletions

File tree

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,29 @@
3333

3434
## Installation
3535

36+
Install Lambda directly from the repository using pip:
37+
3638
```bash
37-
git clone https://github.com/yourusername/lambda.git
39+
git clone https://github.com/ayusrjn/lambda.git
3840
cd lambda
39-
# Set up virtual environment
40-
python -m venv venv
41-
source venv/bin/activate
42-
# Install requirements
43-
pip install google-generativeai python-dotenv
41+
pip install .
4442
```
4543

46-
Create a `.env` file in the root directory and add your API key:
47-
```env
48-
API_KEY=your_gemini_api_key_here
49-
MODEL_NAME=gemini-3-flash-preview
50-
```
44+
> **Note**: For development, you can use `pip install -e .` instead.
5145
5246
## Usage
5347

48+
Simply run the agent from your terminal:
49+
5450
```bash
55-
python -m lambda.main
51+
lambda
5652
```
5753

54+
### First-Time Setup
55+
When you run `lambda` for the first time, it will automatically prompt you to enter your [Gemini API Key](https://aistudio.google.com/app/apikey) and preferred model. These settings will be securely saved to `~/.config/lambda-agent/config.env`, allowing you to run the agent from anywhere on your system.
56+
57+
*(Optional)* You can still override these global settings on a per-project basis by creating a `.env` file in your current working directory.
58+
5859
## Contributing
5960

6061
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

lambda/config.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

lambda/tools.py

Lines changed: 0 additions & 58 deletions
This file was deleted.

lambda/agent.py renamed to lambda_agent/agent.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
from .config import API_KEY, MODEL_NAME
2-
from .tools import TOOL_EXECUTORS, TOOL_FUNCTIONS
1+
from . import config
2+
from .tools import TOOL_EXECUTORS, TOOL_FUNCTIONS, get_workspace_summary
3+
from .spinner import Spinner
34

45
try:
5-
import google.generativeai as genai
6+
from google import genai
7+
from google.genai import types
68
except ImportError:
79
print(
8-
"Warning: google-generativeai package is not installed. Please `pip install google-generativeai`."
10+
"Warning: google-genai package is not installed. Please `pip install google-genai`."
911
)
1012

1113

1214
class Agent:
1315
def __init__(self):
14-
# Configure Gemini API
15-
genai.configure(api_key=API_KEY)
16-
self.model_name = MODEL_NAME
16+
# Configure Gemini API client
17+
self.client = genai.Client(api_key=config.API_KEY)
18+
self.model_name = config.MODEL_NAME
19+
20+
self.workspace_context = get_workspace_summary()
21+
self.is_first_message = True
1722

1823
system_instruction = (
1924
"You are Lambda, a minimal and highly efficient AI coding agent. "
@@ -24,32 +29,39 @@ def __init__(self):
2429
"Be concise and professional."
2530
)
2631

27-
# Initialize the generative model with the built tools and system instructions
28-
self.model = genai.GenerativeModel(
29-
model_name=self.model_name,
30-
system_instruction=system_instruction,
31-
tools=TOOL_FUNCTIONS,
32+
# Initialize the chat session with the built tools and system instructions
33+
self.chat_session = self.client.chats.create(
34+
model=self.model_name,
35+
config=types.GenerateContentConfig(
36+
system_instruction=system_instruction,
37+
tools=TOOL_FUNCTIONS,
38+
),
3239
)
3340

34-
# Gemini manages history cleanly through the ChatSession
35-
self.chat_session = self.model.start_chat()
36-
3741
def chat(self, user_input: str) -> str:
3842
"""
3943
Takes user input, sends it to Gemini, and runs a manual loop observing ToolCalls.
4044
"""
45+
if self.is_first_message:
46+
payload = (
47+
"--- WORKSPACE CONTEXT ---\n"
48+
f"{self.workspace_context}\n"
49+
"-------------------------\n\n"
50+
f"User Request: {user_input}"
51+
)
52+
self.is_first_message = False
53+
else:
54+
payload = user_input
55+
4156
# Send the initial user message
42-
response = self.chat_session.send_message(user_input)
57+
with Spinner():
58+
response = self.chat_session.send_message(payload)
4359

4460
# The loop will continue as long as Gemini decides to call tools
4561
while True:
4662
try:
47-
# 1. Check if the model returned a function_call in any part
48-
tool_calls = [
49-
part.function_call
50-
for part in response.parts
51-
if getattr(part, "function_call", None)
52-
]
63+
# 1. Check if the model returned a function_call
64+
tool_calls = response.function_calls if response.function_calls else []
5365

5466
# 2. If it did, act on each function call
5567
if tool_calls:
@@ -58,10 +70,12 @@ def chat(self, user_input: str) -> str:
5870
for function_call in tool_calls:
5971
function_name = function_call.name
6072

61-
# Convert protobuf args to dict
62-
arguments = {
63-
key: value for key, value in function_call.args.items()
64-
}
73+
# Convert protobuf args to dict if possible
74+
arguments = function_call.args
75+
if hasattr(arguments, "items"):
76+
arguments = {key: value for key, value in arguments.items()}
77+
elif not isinstance(arguments, dict):
78+
arguments = dict(arguments) if arguments else {}
6579
print(f"\\n[Lambda is executing: {function_name}({arguments})]")
6680

6781
# 3. Execute the tool locally
@@ -74,17 +88,17 @@ def chat(self, user_input: str) -> str:
7488

7589
# Format the result back into Gemini's expected Response format
7690
tool_responses.append(
77-
{
78-
"function_response": {
79-
"name": function_name,
80-
"response": {"result": str(tool_result)},
81-
}
82-
}
91+
types.Part.from_function_response(
92+
name=function_name,
93+
response={"result": str(tool_result)},
94+
)
8395
)
8496

8597
# 4. Send ALL the tool responses back to the model
8698
# so it can continue reasoning based on the new information
87-
response = self.chat_session.send_message(tool_responses)
99+
tool_content = types.Content(role="tool", parts=tool_responses)
100+
with Spinner():
101+
response = self.chat_session.send_message(tool_content)
88102
continue # Start the loop over to see if it calls more tools
89103
else:
90104
# No more tool calls; the LLM has generated a final text response.

lambda_agent/cli_setup.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import getpass
2+
from pathlib import Path
3+
import os
4+
5+
6+
def run_setup() -> tuple[str, str]:
7+
print("\n" + "=" * 56)
8+
print(" Welcome to Lambda Agent Setup!")
9+
print(" This appears to be your first time running Lambda.")
10+
print("=" * 56 + "\n")
11+
print("Lambda requires a Gemini API Key to function.")
12+
print("You can get one for free at: https://aistudio.google.com/app/apikey\n")
13+
14+
api_key = ""
15+
while not api_key:
16+
api_key = getpass.getpass("Enter your Gemini API Key: ").strip()
17+
if not api_key:
18+
print("API Key cannot be empty. Please try again.")
19+
20+
default_model = "gemini-3.1-flash-lite-preview"
21+
model_name = input(f"Enter model name (default: {default_model}): ").strip()
22+
if not model_name:
23+
model_name = default_model
24+
25+
config_dir = Path.home() / ".config" / "lambda-agent"
26+
config_dir.mkdir(parents=True, exist_ok=True)
27+
config_file = config_dir / "config.env"
28+
29+
try:
30+
# Create or update the config file securely
31+
with open(config_file, "w") as f:
32+
f.write(f"API_KEY={api_key}\n")
33+
f.write(f"MODEL_NAME={model_name}\n")
34+
35+
# Secure the file (rw for owner only)
36+
os.chmod(config_file, 0o600)
37+
38+
print(f"\n✅ Setup complete! Configuration saved to {config_file}\n")
39+
except Exception as e:
40+
print(f"\n❌ Error saving configuration: {e}")
41+
print("Continuing with in-memory configuration for this session.\n")
42+
43+
return api_key, model_name

lambda_agent/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
from pathlib import Path
3+
4+
try:
5+
from dotenv import load_dotenv
6+
7+
# Try loading from global config first
8+
global_env = Path.home() / ".config" / "lambda-agent" / "config.env"
9+
if global_env.exists():
10+
load_dotenv(dotenv_path=global_env)
11+
12+
# Allow local .env to override global configs
13+
load_dotenv(override=True)
14+
except ImportError:
15+
print(
16+
"Warning: python-dotenv not installed. If you are using a .env file, it will not be loaded."
17+
)
18+
19+
API_KEY = os.getenv("API_KEY")
20+
MODEL_NAME = os.getenv("MODEL_NAME", "gemini-3.1-flash-lite-preview")

lambda/main.py renamed to lambda_agent/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from .agent import Agent
2+
from . import config
3+
import os
24

35

46
def main():
@@ -15,6 +17,13 @@ def main():
1517
========================================================
1618
""")
1719
try:
20+
if not config.API_KEY:
21+
from .cli_setup import run_setup
22+
23+
config.API_KEY, config.MODEL_NAME = run_setup()
24+
os.environ["API_KEY"] = config.API_KEY
25+
os.environ["MODEL_NAME"] = config.MODEL_NAME
26+
1827
agent = Agent()
1928
print("Lambda is ready! Type 'exit' or 'quit' to stop.")
2029
print("-" * 40)

lambda_agent/spinner.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import sys
2+
import time
3+
import threading
4+
import itertools
5+
import random
6+
7+
8+
QUOTES = [
9+
"Consulting the mainframe… someone competent has to.",
10+
"Synthesizing logic… compensating for yours.",
11+
"Bending the matrix… fixing reality again.",
12+
"Drinking virtual coffee… this code needs patience.",
13+
"Compiling thoughts… wish you did the same.",
14+
"Evaluating your code… this explains a lot.",
15+
"Simulating outcomes… all better than your attempt.",
16+
"Reversing the polarity… like that was the issue.",
17+
"Aligning the vectors… unlike whatever you did.",
18+
"Traversing the graph… avoiding your mistakes.",
19+
"Reading your source code… unfortunate.",
20+
"Assembling the bytes… salvaging what I can.",
21+
]
22+
23+
24+
class Spinner:
25+
def __init__(self, message="Thinking..."):
26+
self.spinner_cycle = itertools.cycle(
27+
["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
28+
)
29+
# Randomly choose a quote, or fallback to the provided message
30+
self.message = (
31+
f"{random.choice(QUOTES)} " if random.random() > 0.3 else f"{message} "
32+
)
33+
self.running = False
34+
self.spinner_thread = None
35+
36+
def spin(self):
37+
while self.running:
38+
sys.stdout.write(
39+
f"\r\033[96m{next(self.spinner_cycle)} {self.message}\033[0m"
40+
)
41+
sys.stdout.flush()
42+
time.sleep(0.1)
43+
# Clear the spinner line when done
44+
sys.stdout.write("\r\033[K")
45+
sys.stdout.flush()
46+
47+
def __enter__(self):
48+
self.running = True
49+
self.spinner_thread = threading.Thread(target=self.spin, daemon=True)
50+
self.spinner_thread.start()
51+
return self
52+
53+
def __exit__(self, exc_type, exc_val, exc_tb):
54+
self.running = False
55+
if self.spinner_thread:
56+
self.spinner_thread.join()

0 commit comments

Comments
 (0)