diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c32ecc4..d598c3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,6 +83,17 @@ pytest -v pre-commit run --all-files ``` +### 6. Release a new version +```bash +python release.py +``` + +### 3. Optional: Publish to PyPI +``` +python -m build +twine upload dist/* +``` + ## Quick Checklist Before pushing your changes, ensure: diff --git a/README.md b/README.md index e5101fd..6e61cc5 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,18 @@ The following examples are for reference only. Prefer docs for the latest inform ## Installation +Install Langbase SDK: + ```bash pip install langbase ``` +Install dotenv: + +```bash +pip install dotenv +``` + ## Quick Start ### 1. Set up your API key @@ -54,6 +62,7 @@ llm_api_key = os.getenv("LLM_API_KEY") # Initialize the client langbase = Langbase(api_key=langbase_api_key) +langbase = Langbase(api_key=langbase_api_key) ``` ### 3. Generate text @@ -188,6 +197,7 @@ results = langbase.memories.retrieve( ```python # Run an agent with tools +response = langbase.agent.run( response = langbase.agent.run( model="openai:gpt-4", messages=[{"role": "user", "content": "Search for AI news"}], @@ -202,6 +212,7 @@ response = langbase.agent.run( ```python # Chunk text for processing +chunks = langbase.chunker( chunks = langbase.chunker( content="Long text to split...", chunk_max_length=1024, @@ -209,12 +220,14 @@ chunks = langbase.chunker( ) # Generate embeddings +embeddings = langbase.embed( embeddings = langbase.embed( chunks=["Text 1", "Text 2"], embedding_model="openai:text-embedding-3-small", ) # Parse documents +content = langbase.parser( content = langbase.parser( document=open("document.pdf", "rb"), document_name="document.pdf", @@ -224,13 +237,14 @@ content = langbase.parser( ## Examples -Explore the [examples](./examples) directory for complete working examples: +Explore the [examples](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/examples) directory for complete working examples: -- [Agent with tools](./examples/agent/) -- [Work with memory](./examples/memory/) -- [Generate text](./examples/pipes/pipes.run.py) -- [Document processing](./examples/parser/) -- [Workflow automation](./examples/workflow/) +- [Generate text](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/examples/agent/agent.run.py) +- [Stream text](https://github.com/LangbaseInc/langbase-python-sdk/blob/main/examples/agent/agent.run.stream.py) +- [Work with memory](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/examples/memory/) +- [Agent with tools](https://github.com/LangbaseInc/langbase-python-sdk/blob/main/examples/agent/agent.run.tool.py) +- [Document processing](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/examples/parser/) +- [Workflow automation](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/examples/workflow/) ## SDK Reference @@ -238,7 +252,7 @@ For detailed SDK documentation, visit [langbase.com/docs/sdk](https://langbase.c ## Contributing -We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. +We welcome contributions! Please see our [Contributing Guide](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/CONTRIBUTING.md) for details. ## Support @@ -248,4 +262,4 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +See the [LICENSE](https://github.com/LangbaseInc/langbase-python-sdk/tree/main/LICENSE) file for details. diff --git a/examples/agent/agent.run.py b/examples/agent/agent.run.py index 53514d0..691ee8f 100644 --- a/examples/agent/agent.run.py +++ b/examples/agent/agent.run.py @@ -20,12 +20,12 @@ def main(): if not langbase_api_key: print("❌ Missing LANGBASE_API_KEY in environment variables.") - print("Please set: export LANGBASE_API_KEY='your_langbase_api_key'") + print("Please set: LANGBASE_API_KEY='your_langbase_api_key' in .env file") exit(1) if not llm_api_key: print("❌ Missing LLM_API_KEY in environment variables.") - print("Please set: export LLM_API_KEY='your_llm_api_key'") + print("Please set: LLM_API_KEY='your_llm_api_key' in .env file") exit(1) # Initialize Langbase client diff --git a/examples/agent/agent.run.stream.py b/examples/agent/agent.run.stream.py index 1a82d41..1c83427 100644 --- a/examples/agent/agent.run.stream.py +++ b/examples/agent/agent.run.stream.py @@ -20,7 +20,7 @@ def main(): if not langbase_api_key: print("❌ Missing LANGBASE_API_KEY in environment variables.") - print("Please set: export LANGBASE_API_KEY='your_langbase_api_key'") + print("Please set: LANGBASE_API_KEY='your_langbase_api_key' in .env file") exit(1) # Initialize Langbase client diff --git a/examples/agent/agent.run.workflow.py b/examples/agent/agent.run.workflow.py index b939d12..45bcac9 100644 --- a/examples/agent/agent.run.workflow.py +++ b/examples/agent/agent.run.workflow.py @@ -31,7 +31,7 @@ async def main(): if not llm_api_key: print("❌ Missing LLM_API_KEY in environment variables.") - print("Please set: export LLM_API_KEY='your_llm_api_key'") + print("Please set: LLM_API_KEY='your_llm_api_key' in .env file") exit(1) # Initialize Langbase client and Workflow diff --git a/examples/workflow/email_processing.py b/examples/workflow/email_processing.py index 12da295..e1e18f7 100644 --- a/examples/workflow/email_processing.py +++ b/examples/workflow/email_processing.py @@ -33,12 +33,12 @@ async def process_email(email_content: str): if not langbase_api_key: print("❌ Missing LANGBASE_API_KEY in environment variables.") - print("Please set: export LANGBASE_API_KEY='your_langbase_api_key'") + print("Please set: LANGBASE_API_KEY='your_langbase_api_key' in .env file") exit(1) if not llm_api_key: print("❌ Missing LLM_API_KEY in environment variables.") - print("Please set: export LLM_API_KEY='your_llm_api_key'") + print("Please set: LLM_API_KEY='your_llm_api_key' in .env file") exit(1) # Initialize Langbase diff --git a/examples/workflow/summarization.py b/examples/workflow/summarization.py index 61d7b66..707b76d 100644 --- a/examples/workflow/summarization.py +++ b/examples/workflow/summarization.py @@ -32,12 +32,12 @@ async def process_text(input_text: str): if not langbase_api_key: print("❌ Missing LANGBASE_API_KEY in environment variables.") - print("Please set: export LANGBASE_API_KEY='your_langbase_api_key'") + print("Please set: LANGBASE_API_KEY='your_langbase_api_key' in .env file") exit(1) if not llm_api_key: print("❌ Missing LLM_API_KEY in environment variables.") - print("Please set: export LLM_API_KEY='your_llm_api_key'") + print("Please set: LLM_API_KEY='your_llm_api_key' in .env file") exit(1) # Initialize Langbase diff --git a/langbase/__init__.py b/langbase/__init__.py index 2521030..02989d6 100644 --- a/langbase/__init__.py +++ b/langbase/__init__.py @@ -63,7 +63,10 @@ ) from .workflow import TimeoutError, Workflow -__version__ = "0.1.0" +__version__ = "0.0.0" +__author__ = "LangbaseInc" +__description__ = "Python SDK for the Langbase API" + __all__ = [ # Errors "APIConnectionError", diff --git a/pyproject.toml b/pyproject.toml index 9dc0a65..20c69ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,28 @@ [project] name = "langbase" -version = "0.1.0" +version = "0.0.0" description = "Python SDK for the Langbase API" readme = "README.md" -license = {text = "MIT"} +license = {text = "Apache-2.0"} authors = [ - { name = "Saqib", email = "saqib@langbase.com" }, - { name = "Ankit", email = "ankit@langbase.com" }, + { name = "Saqib Ameen", email = "saqib@langbase.com" }, + { name = "Ankit Kumar", email = "ankit@langbase.com" }, ] requires-python = ">=3.7" -keywords = ["ai", "langbase", "agent", "memory", "rag", "mcp", "pipes", "workflow"] +keywords = ["ai", "langbase", "agent", "memory", "rag", "mcp", "pipes", "workflow", "llms"] classifiers = [ - "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ @@ -27,6 +30,11 @@ dependencies = [ "typing-extensions>=4.0.0", ] +[project.optional-dependencies] +release = [ + "python-semantic-release>=8.0.0", +] + [project.urls] Documentation = "https://docs.langbase.com" Homepage = "https://langbase.com" @@ -102,3 +110,19 @@ precision = 2 [tool.coverage.html] directory = "htmlcov" + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +version_variables = [ + "langbase/__init__.py:__version__", +] +branch = "main" +upload_to_PyPI = false +upload_to_release = false +build_command = "pip install build && python -m build" + +[tool.semantic_release.commit_parser_options] +allowed_tags = ["📦 NEW", "👌 IMPROVE", "🐛 FIX", "🚀 RELEASE", "📖 DOC", "🤖 TEST", "‼️ BREAKING"] +minor_tags = ["📦 NEW"] +patch_tags = ["👌 IMPROVE", "🐛 FIX", "🚀 RELEASE"] +major_tags = ["‼️ BREAKING"] diff --git a/release.py b/release.py new file mode 100644 index 0000000..2c70817 --- /dev/null +++ b/release.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +""" +Interactive release script for Langbase Python SDK. +Usage: python release.py +""" + +import os +import re +import subprocess +import sys +from datetime import datetime + +# Fix Windows encoding issues with emojis +os.environ["PYTHONUTF8"] = "1" +os.environ["PYTHONIOENCODING"] = "utf-8" + + +def run_command(cmd, description, capture_output=True): + """Run a command and handle errors.""" + print(f"🔄 {description}...") + print(f" Running: {cmd}") + try: + result = subprocess.run( + cmd, + shell=True, + check=True, + capture_output=capture_output, + text=True, + encoding="utf-8", + errors="replace", + ) + if capture_output: + # Show both stdout and stderr + if result.stdout.strip(): + print("📤 Output:") + print(result.stdout) + if result.stderr.strip(): + print("⚠️ Warnings:") + print(result.stderr) + if not result.stdout.strip() and not result.stderr.strip(): + print("✅ Command completed (no output)") + return True, result.stdout if capture_output else "" + except subprocess.CalledProcessError as e: + print(f"❌ Error: {e}") + print(f"❌ Command that failed: {cmd}") + if e.stdout: + print(f"📤 Output: {e.stdout}") + if e.stderr: + print(f"📤 Error details: {e.stderr}") + return False, "" + + +def get_current_version(): + """Get current version from pyproject.toml.""" + try: + with open("pyproject.toml", "r", encoding="utf-8") as f: + content = f.read() + match = re.search(r'version = "([^"]+)"', content) + if match: + return match.group(1) + except FileNotFoundError: + pass + return "0.0.0" + + +def parse_version(version): + """Parse version string into major, minor, patch.""" + parts = version.split(".") + if len(parts) != 3: + raise ValueError(f"Invalid version format: {version}") + return int(parts[0]), int(parts[1]), int(parts[2]) + + +def bump_version(current_version, bump_type): + """Bump version based on type.""" + major, minor, patch = parse_version(current_version) + + if bump_type == "major": + return f"{major + 1}.0.0" + elif bump_type == "minor": + return f"{major}.{minor + 1}.0" + elif bump_type == "patch": + return f"{major}.{minor}.{patch + 1}" + else: + raise ValueError(f"Invalid bump type: {bump_type}") + + +def update_version_files(new_version): + """Update version in pyproject.toml and __init__.py.""" + print(f"📝 Updating version to {new_version}...") + + # Update pyproject.toml + try: + with open("pyproject.toml", "r", encoding="utf-8") as f: + content = f.read() + + content = re.sub(r'version = "[^"]+"', f'version = "{new_version}"', content) + + with open("pyproject.toml", "w", encoding="utf-8") as f: + f.write(content) + print("✅ Updated pyproject.toml") + except Exception as e: + print(f"❌ Failed to update pyproject.toml: {e}") + return False + + # Update __init__.py + try: + with open("langbase/__init__.py", "r", encoding="utf-8") as f: + content = f.read() + + content = re.sub( + r'__version__ = "[^"]+"', f'__version__ = "{new_version}"', content + ) + + with open("langbase/__init__.py", "w", encoding="utf-8") as f: + f.write(content) + print("✅ Updated langbase/__init__.py") + except Exception as e: + print(f"❌ Failed to update langbase/__init__.py: {e}") + return False + + return True + + +def update_changelog(version, release_message): + """Update CHANGELOG.md with new release.""" + print("📝 Updating CHANGELOG.md...") + + try: + # Read current changelog + try: + with open("CHANGELOG.md", "r", encoding="utf-8") as f: + current_content = f.read() + except FileNotFoundError: + current_content = "# Changelog\n\n" + + # Create new entry + date = datetime.now().strftime("%Y-%m-%d") + new_entry = f"## [{version}] - {date}\n\n{release_message}\n\n" + + # Insert after the header + if "# Changelog" in current_content: + parts = current_content.split("# Changelog\n", 1) + updated_content = f"# Changelog\n\n{new_entry}" + ( + parts[1] if len(parts) > 1 else "" + ) + else: + updated_content = f"# Changelog\n\n{new_entry}{current_content}" + + with open("CHANGELOG.md", "w", encoding="utf-8") as f: + f.write(updated_content) + + print("✅ Updated CHANGELOG.md") + return True + except Exception as e: + print(f"❌ Failed to update CHANGELOG.md: {e}") + return False + + +def ask_yes_no(question): + """Ask a yes/no question.""" + while True: + try: + answer = input(f"\n❓ {question} (y/n): ").lower().strip() + if answer in ["y", "yes"]: + return True + elif answer in ["n", "no", ""]: + return False + else: + print("Please answer 'y' or 'n'") + except KeyboardInterrupt: + print("\n❌ Operation cancelled") + sys.exit(1) + + +def main(): + """Run the interactive release process.""" + print("🚀 Starting Interactive Langbase SDK Release Process...\n") + + # Ask if this is a test release + test_mode = ask_yes_no( + "Is this a TEST release? (uploads to test.pypi.org instead of PyPI)" + ) + if test_mode: + print("🧪 TEST MODE: Will upload to test.pypi.org") + + # Get current version + current_version = get_current_version() + print(f"📋 Current version: {current_version}") + + # Step 1: Ask for version bump type + print("\n📈 What type of release is this?") + print(" • patch - Bug fixes, small improvements (0.1.0 → 0.1.1)") + print(" • minor - New features, backwards compatible (0.1.0 → 0.2.0)") + print(" • major - Breaking changes (0.1.0 → 1.0.0)") + + while True: + try: + bump_type = ( + input("\n❓ Enter release type (patch/minor/major): ").lower().strip() + ) + if bump_type in ["patch", "minor", "major"]: + break + else: + print("Please enter 'patch', 'minor', or 'major'") + except KeyboardInterrupt: + print("\n❌ Release cancelled") + sys.exit(1) + + # Calculate new version + new_version = bump_version(current_version, bump_type) + print(f"\n📋 New version will be: {current_version} → {new_version}") + + # Step 2: Confirm version update + if not ask_yes_no(f"Update version to {new_version}?"): + print("❌ Release cancelled") + return + + # Step 3: Get release message + print(f"\n📝 Enter release message for v{new_version}:") + print(" (Describe what's new, changed, or fixed)") + try: + release_message = input("Release message: ").strip() + if not release_message: + release_message = f"Release v{new_version}" + except KeyboardInterrupt: + print("\n❌ Release cancelled") + sys.exit(1) + + # Step 4: Show preview and confirm + print(f"\n📋 Release Summary:") + print(f" Version: {current_version} → {new_version}") + print(f" Type: {bump_type}") + print(f" Message: {release_message}") + if test_mode: + print(" 🧪 Destination: Test PyPI (test.pypi.org)") + else: + print(" 🚀 Destination: Production PyPI (pypi.org)") + + if not ask_yes_no("Proceed with this release?"): + print("❌ Release cancelled") + return + + # Step 5: Update version files + if not update_version_files(new_version): + print("❌ Failed to update version files") + sys.exit(1) + + # Step 6: Update changelog + if not update_changelog(new_version, release_message): + print("❌ Failed to update changelog") + sys.exit(1) + + # Step 7: Handle git commits (skip entirely in test mode) + if test_mode: + print("🧪 TEST MODE: Skipping all git operations (no commits, no pushes)") + else: + if not ask_yes_no("Commit version changes to git?"): + print("❌ Release cancelled") + return + + # Commit changes + commit_message = f"🚀 RELEASE: v{new_version}\n\n{release_message}" + success, _ = run_command( + f'git add . && git commit -m "{commit_message}"', + "Committing version changes", + ) + if not success: + print("❌ Failed to commit changes") + sys.exit(1) + + # Step 8: Ask to push to GitHub + if not ask_yes_no("Push changes to GitHub?"): + print("❌ Skipping GitHub push") + else: + success, _ = run_command("git push origin main", "Pushing to GitHub") + if not success: + print("❌ Failed to push to GitHub") + sys.exit(1) + + # Step 9: Ask to build package + if not ask_yes_no("Build package for PyPI?"): + print("❌ Skipping package build") + return + + # Clean previous builds + run_command("rm -rf dist/ build/ *.egg-info/", "Cleaning previous builds") + + # Build package + success, _ = run_command("python -m build", "Building package") + if not success: + print("❌ Failed to build package") + sys.exit(1) + + # Step 10: Ask to upload to PyPI + upload_destination = "Test PyPI" if test_mode else "PyPI" + if not ask_yes_no(f"Upload to {upload_destination}?"): + print(f"❌ Skipping {upload_destination} upload") + print(f"✅ Release v{new_version} prepared successfully!") + if test_mode: + print( + "🎯 To upload to Test PyPI later, run: twine upload --repository testpypi dist/*" + ) + else: + print("🎯 To upload later, run: twine upload dist/*") + return + + # Upload to PyPI or Test PyPI + if test_mode: + upload_cmd = "twine upload --repository testpypi dist/*" + upload_desc = "Uploading to Test PyPI" + else: + upload_cmd = "twine upload dist/*" + upload_desc = "Uploading to PyPI" + + success, _ = run_command(upload_cmd, upload_desc, capture_output=False) + if not success: + print(f"❌ Failed to upload to {upload_destination}") + sys.exit(1) + + print(f"\n🎉 Release v{new_version} completed successfully!") + print("✅ Version updated") + if not test_mode: + print("✅ Changes committed and pushed to GitHub") + if test_mode: + print("✅ Package uploaded to Test PyPI") + print("🔗 View at: https://test.pypi.org/project/langbase/") + print( + "🧪 Test install: pip install --index-url https://test.pypi.org/simple/ langbase" + ) + print( + "⚠️ Version files were updated locally but NOT committed - you may want to reset them!" + ) + print( + "💡 To reset: git checkout -- pyproject.toml langbase/__init__.py CHANGELOG.md" + ) + else: + print("✅ Package uploaded to PyPI") + print("🔗 View at: https://pypi.org/project/langbase/") + + +if __name__ == "__main__": + main() diff --git a/requirements-dev.txt b/requirements-dev.txt index 24b64b2..e52401e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,3 +21,7 @@ twine>=4.0.0 # Development utilities ipdb>=0.13.0 python-dotenv>=0.19.0 + +# release +python-semantic-release>=8.0.0 +twine>=4.0.0