Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into sm/synopsis-tools-ent…
Browse files Browse the repository at this point in the history
…rypoint-refactor

* origin/main:
  feat: support optional params jsonschema conversion in exchange (#188)
  fix: correct context loading from session new/overwrite and resume (#180)
  feat: trying a license checker (#184)
  docs: getting index.md in sync with readme (#183)
  • Loading branch information
salman1993 committed Oct 24, 2024
2 parents 5a8b9ea + 300c4b6 commit dcdf88a
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 25 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: License Check

"on":
pull_request:
paths:
- '**/pyproject.toml'
- '.github/workflows/license-check.yml'
- '.github/workflows/scripts/check_licenses.py'

jobs:
check-licenses:
name: Check Package Licenses
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tomli requests urllib3
- name: Check licenses
run: |
python .github/workflows/scripts/check_licenses.py \
pyproject.toml || exit_code=$?
if [ "${exit_code:-0}" -ne 0 ]; then
echo "::error::Found packages with disallowed licenses"
exit 1
fi
- name: Check Exchange licenses
run: |
python .github/workflows/scripts/check_licenses.py \
packages/exchange/pyproject.toml || exit_code=$?
if [ "${exit_code:-0}" -ne 0 ]; then
echo "::error::Found packages with disallowed licenses in exchange"
exit 1
fi
174 changes: 174 additions & 0 deletions .github/workflows/scripts/check_licenses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python3

from pathlib import Path
import tomli
import sys
import requests
import urllib3
from typing import Dict, List, Optional, Set

# Define allowed licenses and exceptions directly in the script
ALLOWED_LICENSES = {
"MIT",
"BSD-3-Clause",
"Apache-2.0",
"Apache Software License",
"Python Software Foundation License",
"BSD License",
"ISC"
}

# Package-specific exceptions
EXCEPTIONS = {
"ai-exchange": True, # Local workspace package
"tiktoken": True, # Known MIT license with non-standard format
}

class LicenseChecker:
def __init__(self):
self.session = requests.Session()
# Configure session for robust SSL handling
self.session.verify = True
adapter = requests.adapters.HTTPAdapter(
max_retries=urllib3.util.Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]
)
)
self.session.mount('https://', adapter)

def normalize_license(self, license_str: Optional[str]) -> Optional[str]:
"""Normalize license string for comparison."""
if not license_str:
return None

# Convert to uppercase and remove common words and punctuation
normalized = license_str.upper().replace(' LICENSE', '').replace(' LICENCE', '').strip()

# Common substitutions
replacements = {
'APACHE 2.0': 'APACHE-2.0',
'APACHE SOFTWARE LICENSE': 'APACHE-2.0',
'BSD': 'BSD-3-CLAUSE',
'MIT LICENSE': 'MIT',
'PYTHON SOFTWARE FOUNDATION': 'PSF',
}

return replacements.get(normalized, normalized)

def get_package_license(self, package_name: str) -> Optional[str]:
"""Fetch license information from PyPI."""
if package_name in EXCEPTIONS:
return "APPROVED-EXCEPTION"

try:
response = self.session.get(f"https://pypi.org/pypi/{package_name}/json")
response.raise_for_status()
data = response.json()

license_info = (
data['info'].get('license') or
data['info'].get('classifiers', [])
)

if isinstance(license_info, list):
for classifier in license_info:
if classifier.startswith('License :: '):
parts = classifier.split(' :: ')
return parts[-1]

return license_info if isinstance(license_info, str) else None

except requests.exceptions.SSLError as e:
print(f"SSL Error fetching license for {package_name}: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Warning: Could not fetch license for {package_name}: {e}", file=sys.stderr)
return None

def extract_dependencies(self, toml_file: Path) -> List[str]:
"""Extract all dependencies from a TOML file."""
with open(toml_file, 'rb') as f:
data = tomli.load(f)

dependencies = []

# Get direct dependencies
project_deps = data.get('project', {}).get('dependencies', [])
dependencies.extend(self._parse_dependency_strings(project_deps))

# Get dev dependencies
tool_deps = data.get('tool', {}).get('uv', {}).get('dev-dependencies', [])
dependencies.extend(self._parse_dependency_strings(tool_deps))

return list(set(dependencies))

def _parse_dependency_strings(self, deps: List[str]) -> List[str]:
"""Parse dependency strings to extract package names."""
packages = []
for dep in deps:
# Skip workspace references
if dep.endswith('workspace = true}'):
continue

# Handle basic package specifiers
package = dep.split('>=')[0].split('==')[0].split('<')[0].split('>')[0].strip()
package = package.split('{')[0].strip()
packages.append(package)
return packages

def check_licenses(self, toml_file: Path) -> Dict[str, Dict[str, bool]]:
"""Check licenses for all dependencies in the TOML file."""
dependencies = self.extract_dependencies(toml_file)
results = {}
checked = set()

for package in dependencies:
if package in checked:
continue

checked.add(package)

if package in EXCEPTIONS:
results[package] = {
'license': 'Approved Exception',
'allowed': True
}
continue

license_info = self.get_package_license(package)
normalized_license = self.normalize_license(license_info)
allowed = False

if normalized_license:
allowed = (normalized_license in {self.normalize_license(l) for l in ALLOWED_LICENSES} or
package in EXCEPTIONS)

results[package] = {
'license': license_info,
'allowed': allowed
}

return results

def main():
if len(sys.argv) < 2:
print("Usage: check_licenses.py <toml_file>", file=sys.stderr)
sys.exit(1)

toml_file = Path(sys.argv[1])
checker = LicenseChecker()
results = checker.check_licenses(toml_file)

any_disallowed = False
for package, info in sorted(results.items()):
status = "✓" if info['allowed'] else "✗"
print(f"{status} {package}: {info['license']}")
if not info['allowed']:
any_disallowed = True

sys.exit(1 if any_disallowed else 0)

if __name__ == '__main__':
main()
12 changes: 12 additions & 0 deletions .github/workflows/test-events/pull_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"pull_request": {
"head": {
"ref": "test-branch"
},
"base": {
"ref": "main"
},
"number": 123,
"title": "test: Update dependency licenses"
}
}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ You can run goose to do things just as a one off, such as tidying up, and then e
goose run instructions.md
```

You can also use process substitution to provide instructions directly from the command line:

```sh
goose run <(echo "Create a new Python file that prints hello world")
```

This will run until completion as best it can. You can also pass `--resume-session` and it will re-use the first session it finds for context


Expand Down
36 changes: 35 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ You will see the Goose prompt `G❯`:
G❯ type your instructions here exactly as you would tell a developer.
```

Now you are interacting with Goose in conversational sessions - something like a natural language driven code interpreter. The default toolkit allows Goose to take actions through shell commands and file edits. You can interrupt Goose with `CTRL+D` or `ESC+Enter` at any time to help redirect its efforts.
Now you are interacting with Goose in conversational sessions - think of it as like giving direction to a junior developer. The default toolkit allows Goose to take actions through shell commands and file edits. You can interrupt Goose with `CTRL+D` or `ESC+Enter` at any time to help redirect its efforts.

#### Exit the session

Expand All @@ -141,6 +141,23 @@ goose session resume

To see more documentation on the CLI commands currently available to Goose check out the documentation [here][cli]. If you’d like to develop your own CLI commands for Goose, check out the [Contributing document][contributing].

### Running a goose tasks (one off)

You can run goose to do things just as a one off, such as tidying up, and then exiting:

```sh
goose run instructions.md
```

You can also use process substitution to provide instructions directly from the command line:

```sh
goose run <(echo "Create a new Python file that prints hello world")
```

This will run until completion as best it can. You can also pass `--resume-session` and it will re-use the first session it finds for context


### Next steps

Learn how to modify your Goose profiles.yaml file to add and remove functionality (toolkits) and providing context to get the most out of Goose in our [Getting Started Guide][getting-started].
Expand All @@ -151,6 +168,23 @@ We have some experimental IDE integrations for VSCode and JetBrains IDEs:
* https://github.com/square/goose-vscode
* https://github.com/Kvadratni/goose-intellij

## Other ways to run goose

**Want to move out of the terminal and into an IDE?**

We have some experimental IDE integrations for VSCode and JetBrains IDEs:
* https://github.com/square/goose-vscode
* https://github.com/block-open-source/goose-intellij

**Goose as a Github Action**

There is also an experimental Github action to run goose as part of your workflow (for example if you ask it to fix an issue):
https://github.com/marketplace/actions/goose-ai-developer-agent

**With Docker**

There is also a `Dockerfile` in the root of this project you can use if you want to run goose in a sandboxed fashion.

## Getting involved!

There is a lot to do! If you're interested in contributing, a great place to start is picking a `good-first-issue`-labelled ticket from our [issues list][gh-issues]. More details on how to develop Goose can be found in our [Contributing Guide][contributing]. We are a friendly, collaborative group and look forward to working together![^1]
Expand Down
14 changes: 10 additions & 4 deletions src/goose/cli/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from pathlib import Path
from typing import Optional

from langfuse.decorators import langfuse_context
from exchange import Message, Text, ToolResult, ToolUse
from exchange.langfuse_wrapper import observe_wrapper, auth_check
from exchange.langfuse_wrapper import auth_check, observe_wrapper
from langfuse.decorators import langfuse_context
from rich import print
from rich.markdown import Markdown
from rich.panel import Panel
Expand All @@ -21,7 +21,7 @@
from goose.utils import droid, load_plugins
from goose.utils._cost_calculator import get_total_cost_message
from goose.utils._create_exchange import create_exchange
from goose.utils.session_file import is_empty_session, is_existing_session, read_or_create_file, log_messages
from goose.utils.session_file import is_empty_session, is_existing_session, log_messages, read_or_create_file

RESUME_MESSAGE = "I see we were interrupted. How can I help you?"

Expand Down Expand Up @@ -286,9 +286,15 @@ def _prompt_overwrite_session(self) -> None:
print(f"[yellow]Session already exists at {self.session_file_path}.[/]")

choice = OverwriteSessionPrompt.ask("Enter your choice", show_choices=False)
# during __init__ we load the previous context, so we need to
# explicitly clear it
self.exchange.messages.clear()

match choice:
case "y" | "yes":
print("Overwriting existing session")
with open(self.session_file_path, "w") as f:
f.write("")

case "n" | "no":
while True:
Expand All @@ -299,7 +305,7 @@ def _prompt_overwrite_session(self) -> None:
print(f"[yellow]Session '{new_session_name}' already exists[/]")

case "r" | "resume":
self.exchange.messages.extend(self.load_session())
self.exchange.messages.extend(self._get_initial_messages())

def _remove_empty_session(self) -> bool:
"""
Expand Down
Loading

0 comments on commit dcdf88a

Please sign in to comment.