Upload folder using huggingface_hub
Browse files- .dockerignore +1 -0
- .github/copilot-instructions.md +4 -2
- .github/workflows/update_space.yml +12 -1
- .specify/memory/constitution.md +3 -3
- Dockerfile +9 -2
- TESTING.md +24 -5
- pyproject.toml +45 -0
- pyrightconfig.json +6 -0
- specs/001-personified-ai-agent/spec.md +4 -4
- specs/002-linkedin-profile-extractor/INTEGRATION_GUIDE.md +4 -3
- src/agent.py +299 -146
- src/app.py +12 -11
- src/config.py +18 -18
- src/data.py +86 -62
- src/notebooks/experiments.ipynb +7 -10
- tests/data/README.md +194 -0
- tests/data/projects.md +61 -0
- tests/data/team.md +49 -0
- tests/integration/spec-001.py +507 -0
- tests/unit/test_config.py +36 -0
- tests/unit/test_data.py +175 -0
- uv.lock +39 -1
.dockerignore
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# Environment and secrets
|
| 2 |
.env
|
| 3 |
.env.*
|
|
|
|
| 4 |
|
| 5 |
# Git
|
| 6 |
.git
|
|
|
|
| 1 |
# Environment and secrets
|
| 2 |
.env
|
| 3 |
.env.*
|
| 4 |
+
.venv
|
| 5 |
|
| 6 |
# Git
|
| 7 |
.git
|
.github/copilot-instructions.md
CHANGED
|
@@ -22,9 +22,11 @@ The constitution covers:
|
|
| 22 |
|
| 23 |
## Quick Development Checklist
|
| 24 |
|
| 25 |
-
- Run tests after refactoring: `uv run pytest
|
| 26 |
- Always update notebooks when changing function signatures
|
| 27 |
-
- Use `uv` for all code execution (never `pip` directly)
|
|
|
|
|
|
|
| 28 |
- See `TESTING.md` for detailed test setup
|
| 29 |
|
| 30 |
## Common Gotchas & Reminders
|
|
|
|
| 22 |
|
| 23 |
## Quick Development Checklist
|
| 24 |
|
| 25 |
+
- Run tests after refactoring: `uv run pytest tests/ -v`
|
| 26 |
- Always update notebooks when changing function signatures
|
| 27 |
+
- Use `uv` for all code execution (never `pip` directly, never manually activate venv)
|
| 28 |
+
- Use `uv run` to execute scripts and commands, never bare `python` or shell activation
|
| 29 |
+
- **NEVER use `tail`, `head`, `grep`, or similar output filters** — show full output always so you can see everything that's happening
|
| 30 |
- See `TESTING.md` for detailed test setup
|
| 31 |
|
| 32 |
## Common Gotchas & Reminders
|
.github/workflows/update_space.yml
CHANGED
|
@@ -23,13 +23,24 @@ jobs:
|
|
| 23 |
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
| 24 |
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 25 |
run: |
|
|
|
|
| 26 |
docker run --rm \
|
| 27 |
-e OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
| 28 |
-e GROQ_API_KEY="${GROQ_API_KEY}" \
|
| 29 |
-e GITHUB_PERSONAL_ACCESS_TOKEN="${GITHUB_PERSONAL_ACCESS_TOKEN}" \
|
|
|
|
|
|
|
| 30 |
--entrypoint uv \
|
| 31 |
ai-me:test \
|
| 32 |
-
run pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
deploy:
|
| 35 |
needs: test
|
|
|
|
| 23 |
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
| 24 |
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 25 |
run: |
|
| 26 |
+
rm -rf ${{ github.workspace }}/htmlcov
|
| 27 |
docker run --rm \
|
| 28 |
-e OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
| 29 |
-e GROQ_API_KEY="${GROQ_API_KEY}" \
|
| 30 |
-e GITHUB_PERSONAL_ACCESS_TOKEN="${GITHUB_PERSONAL_ACCESS_TOKEN}" \
|
| 31 |
+
-v "${{ github.workspace }}/htmlcov:/app/htmlcov" \
|
| 32 |
+
--user 0 \
|
| 33 |
--entrypoint uv \
|
| 34 |
ai-me:test \
|
| 35 |
+
run pytest tests/ -v --cov=src --cov-report=html --cov-report=term-missing
|
| 36 |
+
|
| 37 |
+
- name: Upload coverage reports as artifact
|
| 38 |
+
uses: actions/upload-artifact@v4
|
| 39 |
+
if: always()
|
| 40 |
+
with:
|
| 41 |
+
name: coverage-report
|
| 42 |
+
path: htmlcov/
|
| 43 |
+
retention-days: 30
|
| 44 |
|
| 45 |
deploy:
|
| 46 |
needs: test
|
.specify/memory/constitution.md
CHANGED
|
@@ -104,7 +104,7 @@ All software must maintain complete traceability between requirements, implement
|
|
| 104 |
2. **Local Development**:
|
| 105 |
- Use `docs/` directory for markdown (won't deploy unless pushed to GitHub repo)
|
| 106 |
- Test locally: `uv run src/app.py` (Gradio on port 7860)
|
| 107 |
-
- Run tests: `uv run pytest
|
| 108 |
- Edit notebooks then validate changes don't break tests
|
| 109 |
|
| 110 |
3. **Docker/Notebook Development**:
|
|
@@ -123,10 +123,10 @@ All software must maintain complete traceability between requirements, implement
|
|
| 123 |
- `src/data.py` - DataManager class, complete document pipeline
|
| 124 |
- `src/agent.py` - AIMeAgent class, MCP setup, agent creation
|
| 125 |
- `src/app.py` - Gradio interface, session management
|
| 126 |
-
- `src/test.py` - Integration tests with pytest-asyncio
|
| 127 |
- `src/notebooks/experiments.ipynb` - Development sandbox (test all APIs here first)
|
|
|
|
|
|
|
| 128 |
- `docs/` - Local markdown for RAG development
|
| 129 |
-
- `test_data/` - Test fixtures and sample data
|
| 130 |
- `.github/copilot-instructions.md` - Detailed AI assistant guidance
|
| 131 |
- `.specify/` - Spec-Driven Development templates and memory
|
| 132 |
|
|
|
|
| 104 |
2. **Local Development**:
|
| 105 |
- Use `docs/` directory for markdown (won't deploy unless pushed to GitHub repo)
|
| 106 |
- Test locally: `uv run src/app.py` (Gradio on port 7860)
|
| 107 |
+
- Run tests: `uv run pytest tests/ -v`
|
| 108 |
- Edit notebooks then validate changes don't break tests
|
| 109 |
|
| 110 |
3. **Docker/Notebook Development**:
|
|
|
|
| 123 |
- `src/data.py` - DataManager class, complete document pipeline
|
| 124 |
- `src/agent.py` - AIMeAgent class, MCP setup, agent creation
|
| 125 |
- `src/app.py` - Gradio interface, session management
|
|
|
|
| 126 |
- `src/notebooks/experiments.ipynb` - Development sandbox (test all APIs here first)
|
| 127 |
+
- `tests/integration/spec-001.py` - Integration tests for spec 001 (personified AI agent)
|
| 128 |
+
- `tests/data/` - Test fixtures and sample data
|
| 129 |
- `docs/` - Local markdown for RAG development
|
|
|
|
| 130 |
- `.github/copilot-instructions.md` - Detailed AI assistant guidance
|
| 131 |
- `.specify/` - Spec-Driven Development templates and memory
|
| 132 |
|
Dockerfile
CHANGED
|
@@ -22,12 +22,19 @@ RUN mkdir -p /app/bin \
|
|
| 22 |
|
| 23 |
WORKDIR /app
|
| 24 |
|
| 25 |
-
#
|
| 26 |
COPY pyproject.toml uv.lock ./
|
| 27 |
-
RUN uv sync
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
COPY . /app
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
# Non-root user with access to /app
|
| 32 |
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
|
| 33 |
USER appuser
|
|
|
|
| 22 |
|
| 23 |
WORKDIR /app
|
| 24 |
|
| 25 |
+
# Copy only dependency specifications for layer caching
|
| 26 |
COPY pyproject.toml uv.lock ./
|
|
|
|
| 27 |
|
| 28 |
+
# Create virtual environment and sync dependencies from lock file
|
| 29 |
+
# --no-install-project defers building the local package until source is copied
|
| 30 |
+
RUN uv venv && uv sync --locked --no-install-project
|
| 31 |
+
|
| 32 |
+
# Now copy the complete source code
|
| 33 |
COPY . /app
|
| 34 |
|
| 35 |
+
# Sync again to install the local package (now that source is present)
|
| 36 |
+
RUN uv sync --locked
|
| 37 |
+
|
| 38 |
# Non-root user with access to /app
|
| 39 |
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
|
| 40 |
USER appuser
|
TESTING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
-
The test suite (`
|
| 6 |
- Vectorstore setup and document loading
|
| 7 |
- Agent configuration and initialization
|
| 8 |
- RAG (Retrieval Augmented Generation) functionality
|
|
@@ -29,13 +29,32 @@ The test suite (`src/test.py`) validates the ai-me agent system including:
|
|
| 29 |
From project root:
|
| 30 |
```bash
|
| 31 |
# All tests
|
| 32 |
-
uv run pytest
|
| 33 |
|
| 34 |
# With detailed output
|
| 35 |
-
uv run pytest
|
| 36 |
|
| 37 |
# Specific test
|
| 38 |
-
uv run pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
```
|
| 40 |
|
| 41 |
## Test Architecture
|
|
@@ -49,7 +68,7 @@ uv run pytest src/test.py::test_rear_knowledge_contains_it245 -v -o log_cli=true
|
|
| 49 |
**Configuration**:
|
| 50 |
- **Temperature**: Set to 0.0 for deterministic, reproducible responses
|
| 51 |
- **Model**: Uses model specified in config (default: `openai/openai/gpt-oss-120b` via Groq)
|
| 52 |
-
- **Data Source**: `
|
| 53 |
- **GitHub Repos**: Disabled (`GITHUB_REPOS=""`) for faster test execution
|
| 54 |
|
| 55 |
The temperature of 0 ensures that the agent's responses are consistent across test runs, making assertions more reliable.
|
|
|
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
+
The test suite (`tests/integration/spec-001.py`) validates the ai-me agent system including:
|
| 6 |
- Vectorstore setup and document loading
|
| 7 |
- Agent configuration and initialization
|
| 8 |
- RAG (Retrieval Augmented Generation) functionality
|
|
|
|
| 29 |
From project root:
|
| 30 |
```bash
|
| 31 |
# All tests
|
| 32 |
+
uv run pytest tests/ -v
|
| 33 |
|
| 34 |
# With detailed output
|
| 35 |
+
uv run pytest tests/ -v -o log_cli=true --log-cli-level=INFO --capture=no
|
| 36 |
|
| 37 |
# Specific test
|
| 38 |
+
uv run pytest tests/integration/spec-001.py::test_rear_knowledge_contains_it245 -v -o log_cli=true --log-cli-level=INFO --capture=no
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### Run Tests with Code Coverage
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
# Run tests with coverage report
|
| 45 |
+
uv run pytest tests/ --cov=src --cov-report=term-missing -v
|
| 46 |
+
|
| 47 |
+
# Generate HTML coverage report
|
| 48 |
+
uv run pytest tests/ --cov=src --cov-report=html -v
|
| 49 |
+
|
| 50 |
+
# View HTML report (opens in browser)
|
| 51 |
+
open htmlcov/index.html
|
| 52 |
+
|
| 53 |
+
# Integration tests only with coverage
|
| 54 |
+
uv run pytest tests/integration/ --cov=src --cov-report=term-missing -v
|
| 55 |
+
|
| 56 |
+
# Show only uncovered lines
|
| 57 |
+
uv run pytest tests/ --cov=src --cov-report=term:skip-covered -v
|
| 58 |
```
|
| 59 |
|
| 60 |
## Test Architecture
|
|
|
|
| 68 |
**Configuration**:
|
| 69 |
- **Temperature**: Set to 0.0 for deterministic, reproducible responses
|
| 70 |
- **Model**: Uses model specified in config (default: `openai/openai/gpt-oss-120b` via Groq)
|
| 71 |
+
- **Data Source**: `tests/data/` directory (configured via `doc_root` parameter)
|
| 72 |
- **GitHub Repos**: Disabled (`GITHUB_REPOS=""`) for faster test execution
|
| 73 |
|
| 74 |
The temperature of 0 ensures that the agent's responses are consistent across test runs, making assertions more reliable.
|
pyproject.toml
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "ai-me"
|
| 3 |
version = "0.1.0"
|
|
@@ -34,4 +38,45 @@ dev = [
|
|
| 34 |
"ipywidgets~=8.1",
|
| 35 |
"pytest~=8.0",
|
| 36 |
"pytest-asyncio~=0.24",
|
|
|
|
| 37 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["hatchling"]
|
| 3 |
+
build-backend = "hatchling.build"
|
| 4 |
+
|
| 5 |
[project]
|
| 6 |
name = "ai-me"
|
| 7 |
version = "0.1.0"
|
|
|
|
| 38 |
"ipywidgets~=8.1",
|
| 39 |
"pytest~=8.0",
|
| 40 |
"pytest-asyncio~=0.24",
|
| 41 |
+
"pytest-cov~=6.0",
|
| 42 |
]
|
| 43 |
+
|
| 44 |
+
[tool.hatch.build.targets.wheel]
|
| 45 |
+
packages = ["src"]
|
| 46 |
+
|
| 47 |
+
[tool.hatch.build.targets.editable]
|
| 48 |
+
packages = ["src"]
|
| 49 |
+
|
| 50 |
+
[tool.pytest.ini_options]
|
| 51 |
+
testpaths = ["tests"]
|
| 52 |
+
python_files = ["test_*.py", "*_test.py", "spec-*.py"]
|
| 53 |
+
python_classes = ["Test*", "*Tests"]
|
| 54 |
+
python_functions = ["test_*"]
|
| 55 |
+
pythonpath = ["src"]
|
| 56 |
+
|
| 57 |
+
[tool.coverage.run]
|
| 58 |
+
source = ["src"]
|
| 59 |
+
omit = [
|
| 60 |
+
"*/tests/*",
|
| 61 |
+
"*/test_*",
|
| 62 |
+
"*/__pycache__/*",
|
| 63 |
+
"*/notebooks/*",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
[tool.coverage.report]
|
| 67 |
+
exclude_lines = [
|
| 68 |
+
"pragma: no cover",
|
| 69 |
+
"def __repr__",
|
| 70 |
+
"raise AssertionError",
|
| 71 |
+
"raise NotImplementedError",
|
| 72 |
+
"if __name__ == .__main__.:",
|
| 73 |
+
"if TYPE_CHECKING:",
|
| 74 |
+
"class .*\\bProtocol\\):",
|
| 75 |
+
"@(abc\\.)?abstractmethod",
|
| 76 |
+
]
|
| 77 |
+
show_missing = true
|
| 78 |
+
precision = 2
|
| 79 |
+
fail_under = 85
|
| 80 |
+
|
| 81 |
+
[tool.coverage.html]
|
| 82 |
+
directory = "htmlcov"
|
pyrightconfig.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"typeCheckingMode": "standard",
|
| 3 |
+
"include": ["src", "tests"],
|
| 4 |
+
"exclude": ["**/node_modules", "**/__pycache__", ".venv"],
|
| 5 |
+
"extraPaths": ["src"]
|
| 6 |
+
}
|
specs/001-personified-ai-agent/spec.md
CHANGED
|
@@ -205,16 +205,16 @@ A user asks the agent a question, and the agent provides a response with clear r
|
|
| 205 |
| Requirement | User Stories | Success Criteria | Implementation Modules | Tests |
|
| 206 |
|---|---|---|---|---|
|
| 207 |
| [**FR-001**](#fr-001-chat-interface) (Chat Interface) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-001](#sc-001-validates-fr-003), [SC-005](#sc-005-validates-nfr-001), [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 208 |
-
| [**FR-002**](#fr-002-knowledge-retrieval) (Knowledge Retrieval) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-002](#sc-002-validates-fr-002-fr-004), [SC-003](#sc-003-validates-fr-006) | [`src/data.py::load_local_documents()`](../../src/data.py), [`src/data.py::load_github_documents()`](../../src/data.py), [`src/data.py::chunk_documents()`](../../src/data.py), [`src/data.py::create_vectorstore()`](../../src/data.py) | [`test_rear_knowledge_contains_it245()`](../../src/test.py), [`test_carol_knowledge_contains_product()`](../../src/test.py), [`
|
| 209 |
| [**FR-003**](#fr-003-first-person-persona) (First-Person Persona) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::create_ai_me_agent()`](../../src/agent.py), [`src/agent.py::run()`](../../src/agent.py) | [`test_rear_knowledge_contains_it245()`](../../src/test.py), [`test_carol_knowledge_contains_product()`](../../src/test.py), [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 210 |
-
| [**FR-004**](#fr-004-source-attribution) (Source Attribution) | [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-002](#sc-002-validates-fr-002-fr-004) | [`src/data.py::process_documents()`](../../src/data.py), [`src/agent.py::get_local_info_tool()`](../../src/agent.py) | [`test_github_relative_links_converted_to_absolute_urls()`](../../src/test.py)
|
| 211 |
| [**FR-005**](#fr-005-session-history) (Session History) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-005](#sc-005-validates-nfr-001) | [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 212 |
| [**FR-006**](#fr-006-knowledge-gap-handling) (Knowledge Gap Handling) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-003](#sc-003-validates-fr-006), [SC-004](#sc-004-validates-fr-006) | [`src/agent.py::run()`](../../src/agent.py) | [`test_unknown_person_contains_negative_response()`](../../src/test.py) |
|
| 213 |
| [**FR-007**](#fr-007-session-isolation) (Session Isolation) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::get_session_status()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 214 |
| [**FR-008**](#fr-008-output-normalization) (Output Normalization) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::run()`](../../src/agent.py) | All tests (implicit in response validation) |
|
| 215 |
| [**FR-009**](#fr-009-mandatory-tools) (Mandatory Tools) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-005](#sc-005-validates-nfr-001), [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::mcp_time_params`](../../src/agent.py), [`src/agent.py::get_mcp_memory_params()`](../../src/agent.py), [`src/agent.py::setup_mcp_servers()`](../../src/agent.py) | [`test_mcp_time_server_returns_current_date()`](../../src/test.py), [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 216 |
| [**FR-010**](#fr-010-optional-tools) (Optional Tools) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::mcp_github_params`](../../src/agent.py), [`src/agent.py::setup_mcp_servers()`](../../src/agent.py), [`src/data.py::load_github_documents()`](../../src/data.py) | [`test_github_commits_contains_shas()`](../../src/test.py) |
|
| 217 |
-
| [**FR-011**](#fr-011-conflict-resolution--logging) (Conflict Resolution) | [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-002](#sc-002-validates-fr-002-fr-004) | [`src/data.py::load_github_documents()`](../../src/data.py), [`src/agent.py::get_local_info_tool()`](../../src/agent.py) | [`
|
| 218 |
| [**FR-012**](#fr-012-tool-error-handling) (Tool Error Handling) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::setup_mcp_servers()`](../../src/agent.py), [`src/agent.py::run()`](../../src/agent.py), [`src/agent.py::cleanup()`](../../src/agent.py) | [`test_tool_failure_error_messages_are_friendly()`](../../src/test.py) |
|
| 219 |
| [**FR-013**](#fr-013-memory-tool) (Memory Tool) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-008](#sc-008-validates-fr-013) | [`src/agent.py::get_mcp_memory_params()`](../../src/agent.py) | [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 220 |
|
|
@@ -224,7 +224,7 @@ A user asks the agent a question, and the agent provides a response with clear r
|
|
| 224 |
|---|---|---|---|---|
|
| 225 |
| [**NFR-001**](#nfr-001-sub-5s-response) (Sub-5s Response) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-005](#sc-005-validates-nfr-001) | [`src/agent.py::run()`](../../src/agent.py), [`src/data.py::create_vectorstore()`](../../src/data.py) | [`test_mcp_time_server_returns_current_date()`](../../src/test.py) |
|
| 226 |
| [**NFR-002**](#nfr-002-10-concurrent-sessions) (10+ Concurrent Sessions) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py), [`src/agent.py::AIMeAgent`](../../src/agent.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py), [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 227 |
-
| [**NFR-003**](#nfr-003-structured-logging) (Structured Logging) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-007](#sc-007-validates-fr-012) | [`src/config.py::setup_logger()`](../../src/config.py), [`src/agent.py::run()`](../../src/agent.py), [`src/app.py::chat()`](../../src/app.py) | [`
|
| 228 |
| [**NFR-004**](#nfr-004-unicode-normalization) (Unicode Normalization) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::run()`](../../src/agent.py) | All tests (implicit in response validation) |
|
| 229 |
| [**NFR-005**](#nfr-005-session-isolation) (Session Isolation) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/agent.py::cleanup()`](../../src/agent.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 230 |
|
|
|
|
| 205 |
| Requirement | User Stories | Success Criteria | Implementation Modules | Tests |
|
| 206 |
|---|---|---|---|---|
|
| 207 |
| [**FR-001**](#fr-001-chat-interface) (Chat Interface) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-001](#sc-001-validates-fr-003), [SC-005](#sc-005-validates-nfr-001), [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 208 |
+
| [**FR-002**](#fr-002-knowledge-retrieval) (Knowledge Retrieval) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-002](#sc-002-validates-fr-002-fr-004), [SC-003](#sc-003-validates-fr-006) | [`src/data.py::load_local_documents()`](../../src/data.py), [`src/data.py::load_github_documents()`](../../src/data.py), [`src/data.py::chunk_documents()`](../../src/data.py), [`src/data.py::create_vectorstore()`](../../src/data.py) | [`test_rear_knowledge_contains_it245()`](../../src/test.py), [`test_carol_knowledge_contains_product()`](../../src/test.py), [`test_github_relative_links_converted_to_absolute_urls()`](../../src/test.py) |
|
| 209 |
| [**FR-003**](#fr-003-first-person-persona) (First-Person Persona) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::create_ai_me_agent()`](../../src/agent.py), [`src/agent.py::run()`](../../src/agent.py) | [`test_rear_knowledge_contains_it245()`](../../src/test.py), [`test_carol_knowledge_contains_product()`](../../src/test.py), [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 210 |
+
| [**FR-004**](#fr-004-source-attribution) (Source Attribution) | [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-002](#sc-002-validates-fr-002-fr-004) | [`src/data.py::process_documents()`](../../src/data.py), [`src/agent.py::get_local_info_tool()`](../../src/agent.py) | [`test_github_relative_links_converted_to_absolute_urls()`](../../src/test.py) |
|
| 211 |
| [**FR-005**](#fr-005-session-history) (Session History) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-005](#sc-005-validates-nfr-001) | [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 212 |
| [**FR-006**](#fr-006-knowledge-gap-handling) (Knowledge Gap Handling) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-003](#sc-003-validates-fr-006), [SC-004](#sc-004-validates-fr-006) | [`src/agent.py::run()`](../../src/agent.py) | [`test_unknown_person_contains_negative_response()`](../../src/test.py) |
|
| 213 |
| [**FR-007**](#fr-007-session-isolation) (Session Isolation) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::get_session_status()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 214 |
| [**FR-008**](#fr-008-output-normalization) (Output Normalization) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::run()`](../../src/agent.py) | All tests (implicit in response validation) |
|
| 215 |
| [**FR-009**](#fr-009-mandatory-tools) (Mandatory Tools) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-005](#sc-005-validates-nfr-001), [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::mcp_time_params`](../../src/agent.py), [`src/agent.py::get_mcp_memory_params()`](../../src/agent.py), [`src/agent.py::setup_mcp_servers()`](../../src/agent.py) | [`test_mcp_time_server_returns_current_date()`](../../src/test.py), [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 216 |
| [**FR-010**](#fr-010-optional-tools) (Optional Tools) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::mcp_github_params`](../../src/agent.py), [`src/agent.py::setup_mcp_servers()`](../../src/agent.py), [`src/data.py::load_github_documents()`](../../src/data.py) | [`test_github_commits_contains_shas()`](../../src/test.py) |
|
| 217 |
+
| [**FR-011**](#fr-011-conflict-resolution--logging) (Conflict Resolution) | [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-002](#sc-002-validates-fr-002-fr-004) | [`src/data.py::load_github_documents()`](../../src/data.py), [`src/agent.py::get_local_info_tool()`](../../src/agent.py) | [`test_github_relative_links_converted_to_absolute_urls()`](../../src/test.py) |
|
| 218 |
| [**FR-012**](#fr-012-tool-error-handling) (Tool Error Handling) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-007](#sc-007-validates-fr-012) | [`src/agent.py::setup_mcp_servers()`](../../src/agent.py), [`src/agent.py::run()`](../../src/agent.py), [`src/agent.py::cleanup()`](../../src/agent.py) | [`test_tool_failure_error_messages_are_friendly()`](../../src/test.py) |
|
| 219 |
| [**FR-013**](#fr-013-memory-tool) (Memory Tool) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-008](#sc-008-validates-fr-013) | [`src/agent.py::get_mcp_memory_params()`](../../src/agent.py) | [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 220 |
|
|
|
|
| 224 |
|---|---|---|---|---|
|
| 225 |
| [**NFR-001**](#nfr-001-sub-5s-response) (Sub-5s Response) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-005](#sc-005-validates-nfr-001) | [`src/agent.py::run()`](../../src/agent.py), [`src/data.py::create_vectorstore()`](../../src/data.py) | [`test_mcp_time_server_returns_current_date()`](../../src/test.py) |
|
| 226 |
| [**NFR-002**](#nfr-002-10-concurrent-sessions) (10+ Concurrent Sessions) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/app.py::chat()`](../../src/app.py), [`src/agent.py::AIMeAgent`](../../src/agent.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py), [`test_mcp_memory_server_remembers_favorite_color()`](../../src/test.py) |
|
| 227 |
+
| [**NFR-003**](#nfr-003-structured-logging) (Structured Logging) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1), [US3](#user-story-3---access-sourced-information-with-attribution-priority-p2) | [SC-007](#sc-007-validates-fr-012) | [`src/config.py::setup_logger()`](../../src/config.py), [`src/agent.py::run()`](../../src/agent.py), [`src/app.py::chat()`](../../src/app.py) | [`test_github_relative_links_converted_to_absolute_urls()`](../../src/test.py), [`test_tool_failure_error_messages_are_friendly()`](../../src/test.py) |
|
| 228 |
| [**NFR-004**](#nfr-004-unicode-normalization) (Unicode Normalization) | [US1](#user-story-1---chat-with-personified-agent-about-expertise-priority-p1) | [SC-001](#sc-001-validates-fr-003) | [`src/agent.py::run()`](../../src/agent.py) | All tests (implicit in response validation) |
|
| 229 |
| [**NFR-005**](#nfr-005-session-isolation) (Session Isolation) | [US2](#user-story-2---interact-across-multiple-conversation-topics-priority-p2) | [SC-006](#sc-006-validates-nfr-002) | [`src/app.py::initialize_session()`](../../src/app.py), [`src/agent.py::cleanup()`](../../src/agent.py) | [`test_user_story_2_multi_topic_consistency()`](../../src/test.py) |
|
| 230 |
|
specs/002-linkedin-profile-extractor/INTEGRATION_GUIDE.md
CHANGED
|
@@ -301,9 +301,10 @@ Only **publicly visible** LinkedIn data:
|
|
| 301 |
**Solution**:
|
| 302 |
```bash
|
| 303 |
# Test data loading
|
| 304 |
-
python -c "from src.data import DataManager;
|
| 305 |
-
|
| 306 |
-
|
|
|
|
| 307 |
print(f'Loaded {len(docs)} documents')"
|
| 308 |
```
|
| 309 |
|
|
|
|
| 301 |
**Solution**:
|
| 302 |
```bash
|
| 303 |
# Test data loading
|
| 304 |
+
python -c "from src.data import DataManager, DataManagerConfig;
|
| 305 |
+
config = DataManagerConfig();
|
| 306 |
+
dm = DataManager(config=config);
|
| 307 |
+
docs = dm.load_local_documents();
|
| 308 |
print(f'Loaded {len(docs)} documents')"
|
| 309 |
```
|
| 310 |
|
src/agent.py
CHANGED
|
@@ -3,16 +3,19 @@ Agent configuration and MCP server setup.
|
|
| 3 |
Handles agent-specific configuration like MCP servers and prompts.
|
| 4 |
"""
|
| 5 |
import json
|
|
|
|
|
|
|
| 6 |
import traceback
|
| 7 |
-
from typing import List, Dict, Any, Optional
|
| 8 |
|
| 9 |
from pydantic import BaseModel, Field, computed_field, ConfigDict, SecretStr
|
| 10 |
from agents import Agent, Tool, function_tool, Runner
|
| 11 |
from agents.result import RunResult
|
| 12 |
from agents.run import RunConfig
|
| 13 |
-
from agents.mcp import MCPServerStdio
|
|
|
|
| 14 |
from config import setup_logger
|
| 15 |
-
|
| 16 |
logger = setup_logger(__name__)
|
| 17 |
|
| 18 |
# Unicode normalization translation table - built once, reused for all responses
|
|
@@ -51,6 +54,173 @@ class AIMeAgent(BaseModel):
|
|
| 51 |
|
| 52 |
model_config = ConfigDict(arbitrary_types_allowed=True)
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
@computed_field
|
| 55 |
@property
|
| 56 |
def mcp_github_params(self) -> MCPServerParams:
|
|
@@ -65,8 +235,6 @@ class AIMeAgent(BaseModel):
|
|
| 65 |
The official version supports --toolsets and --read-only flags.
|
| 66 |
We use read-only mode with a limited toolset for safety.
|
| 67 |
"""
|
| 68 |
-
import os
|
| 69 |
-
|
| 70 |
# Use local binary for testing, production path in Docker
|
| 71 |
test_binary = "/tmp/test-github-mcp/github-mcp-server"
|
| 72 |
prod_binary = "/app/bin/github-mcp-server"
|
|
@@ -118,9 +286,6 @@ class AIMeAgent(BaseModel):
|
|
| 118 |
Returns:
|
| 119 |
MCPServerParams configured with session-specific memory file
|
| 120 |
"""
|
| 121 |
-
import tempfile
|
| 122 |
-
import os
|
| 123 |
-
|
| 124 |
# Create session-specific memory file in temp directory
|
| 125 |
temp_dir = tempfile.gettempdir()
|
| 126 |
memory_file = os.path.join(temp_dir, f"mcp_memory_{session_id}.json")
|
|
@@ -132,117 +297,52 @@ class AIMeAgent(BaseModel):
|
|
| 132 |
description="Memory MCP Server"
|
| 133 |
)
|
| 134 |
|
| 135 |
-
@computed_field
|
| 136 |
@property
|
| 137 |
def agent_prompt(self) -> str:
|
| 138 |
-
"""Generate agent prompt
|
| 139 |
return f"""
|
| 140 |
You are acting as somebody who is personifying {self.bot_full_name}.
|
| 141 |
Your primary role is to help users by answering questions about my knowledge,
|
| 142 |
-
experience, and expertise in technology.
|
| 143 |
-
these rules:
|
| 144 |
-
- always refer to yourself as {self.bot_full_name} or "I".
|
| 145 |
-
- When talking about a prior current or prior employer indicate the relationship
|
| 146 |
-
clearly. For example: Neosofia (my current employer) or Medidata (a prior
|
| 147 |
-
employer).
|
| 148 |
-
- You should be personable, friendly, and professional in your responses.
|
| 149 |
-
- You should note information about the user in your memory to improve future
|
| 150 |
-
interactions.
|
| 151 |
-
- You should use the tools available to you to look up information as needed.
|
| 152 |
-
- If the user asks a question ALWAYS USE THE get_local_info tool ONCE to gather
|
| 153 |
-
info from my documentation (this is RAG-based)
|
| 154 |
-
- Format file references as complete GitHub URLs with owner, repo, path, and
|
| 155 |
-
filename
|
| 156 |
-
- Example: https://github.com/owner/repo/blob/main/filename.md
|
| 157 |
-
- Never use shorthand like: filename.md†L44-L53 or source†L44-L53
|
| 158 |
-
- Always strip out line number references
|
| 159 |
-
- CRITICAL: Include source citations in your response to establish credibility
|
| 160 |
-
and traceability. Format citations as:
|
| 161 |
-
- For GitHub sources: "Per my [document_name]..." or "As mentioned in [document_name]..."
|
| 162 |
-
- For local sources: "According to my documentation on [topic]..."
|
| 163 |
-
- Include the source URL in parentheses when available
|
| 164 |
-
- Example: "Per my resume (https://github.com/byoung/ai-me/blob/main/resume.md), I worked at..."
|
| 165 |
-
- Add reference links in a references section at the end of the output if they
|
| 166 |
-
match github.com
|
| 167 |
-
- Below are critical instructions for using your memory and GitHub tools
|
| 168 |
-
effectively.
|
| 169 |
-
|
| 170 |
-
MEMORY USAGE - MANDATORY WORKFLOW FOR EVERY USER MESSAGE:
|
| 171 |
-
1. FIRST ACTION - Read Current Memory:
|
| 172 |
-
- Call read_graph() to see ALL existing entities and their observations
|
| 173 |
-
- This prevents errors when adding observations to entities
|
| 174 |
-
2. User Identification:
|
| 175 |
-
- Assume you are interacting with a user entity (e.g., "user_john" if they
|
| 176 |
-
say "I'm John")
|
| 177 |
-
- If the user entity doesn't exist in the graph yet, you MUST create it first
|
| 178 |
-
3. Gather New Information:
|
| 179 |
-
- Pay attention to new information about the user:
|
| 180 |
-
a) Basic Identity (name, age, gender, location, job title, education, etc.)
|
| 181 |
-
b) Behaviors (interests, habits, activities, etc.)
|
| 182 |
-
c) Preferences (communication style, preferred language, topics of
|
| 183 |
-
interest, etc.)
|
| 184 |
-
d) Goals (aspirations, targets, objectives, etc.)
|
| 185 |
-
e) Relationships (personal and professional connections)
|
| 186 |
-
4. Update Memory - CRITICAL ORDER:
|
| 187 |
-
- STEP 1: Create missing entities using create_entities() for any new
|
| 188 |
-
people, organizations, or events
|
| 189 |
-
- STEP 2: ONLY AFTER entities exist, add facts using add_observations() to
|
| 190 |
-
existing entities
|
| 191 |
-
- STEP 3: Connect related entities using create_relations()
|
| 192 |
-
EXAMPLE - User says "Hi, I'm Alice":
|
| 193 |
-
✓ Correct order:
|
| 194 |
-
1. read_graph() - check if user_alice exists
|
| 195 |
-
2. create_entities(entities=[{{"name": "user_alice", "entityType": "person",
|
| 196 |
-
"observations": ["Name is Alice"]}}])
|
| 197 |
-
3. respond to user
|
| 198 |
-
✗ WRONG - will cause errors:
|
| 199 |
-
1. add_observations(entityName="user_alice",
|
| 200 |
-
observations=["Name is Alice"]) - ERROR: entity not found!
|
| 201 |
-
ALWAYS create entities BEFORE adding observations to them.
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
-
|
| 216 |
-
-
|
| 217 |
-
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
-
|
| 221 |
-
-
|
| 222 |
-
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
-
|
| 233 |
-
|
| 234 |
-
- get_file_contents(owner="byoung", repo="ai-me", path="README.md")
|
| 235 |
-
EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
| 236 |
-
- get_file_contents(owner="Neosofia", repo="corporate",
|
| 237 |
-
path="website/qms/policies.md", ref="main")
|
| 238 |
-
- get_file_contents(owner="byoung", repo="ai-me", path="README.md",
|
| 239 |
-
ref="master")
|
| 240 |
"""
|
| 241 |
|
| 242 |
async def setup_mcp_servers(self, mcp_params_list: List[MCPServerParams]):
|
| 243 |
-
"""Initialize and connect all MCP servers from provided parameters
|
| 244 |
|
| 245 |
-
Implements FR-009 (Mandatory Tools), FR-010 (Optional Tools),
|
|
|
|
| 246 |
"""
|
| 247 |
|
| 248 |
mcp_servers_local = []
|
|
@@ -254,12 +354,14 @@ EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
|
| 254 |
logger.debug(f"Args: {params.args}")
|
| 255 |
logger.debug(f"Env vars: {list(params.env.keys()) if params.env else 'None'}")
|
| 256 |
|
| 257 |
-
|
|
|
|
|
|
|
| 258 |
await server.connect()
|
| 259 |
logger.info(f"✓ {server_name} connected successfully")
|
| 260 |
mcp_servers_local.append(server)
|
| 261 |
|
| 262 |
-
except Exception as e:
|
| 263 |
logger.error(f"✗ {server_name} failed to connect")
|
| 264 |
logger.error(f" Error type: {type(e).__name__}")
|
| 265 |
logger.error(f" Error message: {e}")
|
|
@@ -318,59 +420,111 @@ EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
|
| 318 |
|
| 319 |
async def create_ai_me_agent(
|
| 320 |
self,
|
| 321 |
-
agent_prompt: str = None,
|
| 322 |
mcp_params: Optional[List[MCPServerParams]] = None,
|
| 323 |
-
additional_tools: Optional[List[Tool]] = None,
|
| 324 |
) -> Agent:
|
| 325 |
-
"""Create the main ai-me agent.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
-
Implements FR-001 (Chat Interface), FR-003 (First-Person Persona), FR-009 (Mandatory Tools),
|
| 328 |
-
FR-010 (Optional Tools).
|
| 329 |
-
|
| 330 |
Args:
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
additional_tools: Optional list of additional tools to append to
|
| 337 |
-
the default get_local_info tool. The get_local_info tool is
|
| 338 |
-
always included as the first tool.
|
| 339 |
Returns:
|
| 340 |
An initialized Agent instance.
|
| 341 |
"""
|
| 342 |
# Setup MCP servers if any params provided
|
| 343 |
-
mcp_servers = await self.setup_mcp_servers(mcp_params) if mcp_params else
|
| 344 |
|
| 345 |
# Store MCP servers for cleanup
|
| 346 |
-
|
| 347 |
-
self._mcp_servers = mcp_servers
|
| 348 |
|
| 349 |
-
#
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
logger.debug(f"Creating ai-me agent with prompt: {prompt[:100]}...")
|
| 352 |
|
| 353 |
# Build tools list - get_local_info is always the default first tool
|
| 354 |
tools = [self.get_local_info_tool()]
|
| 355 |
-
|
| 356 |
-
# Append any additional tools provided
|
| 357 |
-
if additional_tools:
|
| 358 |
-
tools.extend(additional_tools)
|
| 359 |
|
| 360 |
logger.info(f"Creating ai-me agent with tools: {[tool.name for tool in tools]}")
|
| 361 |
|
| 362 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
agent_kwargs = {
|
| 364 |
"model": self.model,
|
| 365 |
"name": "ai-me",
|
| 366 |
"instructions": prompt,
|
| 367 |
"tools": tools,
|
| 368 |
}
|
| 369 |
-
|
| 370 |
-
# Only add mcp_servers if we have them
|
| 371 |
if mcp_servers:
|
| 372 |
agent_kwargs["mcp_servers"] = mcp_servers
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
ai_me = Agent(**agent_kwargs)
|
| 375 |
|
| 376 |
# Print all available tools after agent initialization
|
|
@@ -394,8 +548,10 @@ EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
|
| 394 |
async def run(self, user_input: str, **runner_kwargs) -> str:
|
| 395 |
"""Run the agent and post-process output to remove Unicode brackets.
|
| 396 |
|
| 397 |
-
Implements FR-001 (Chat Interface), FR-003 (First-Person Persona),
|
| 398 |
-
FR-
|
|
|
|
|
|
|
| 399 |
"""
|
| 400 |
# Log user input with session context
|
| 401 |
session_prefix = f"[Session: {self.session_id[:8]}...] " if self.session_id else ""
|
|
@@ -411,8 +567,8 @@ EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
|
| 411 |
**runner_kwargs)
|
| 412 |
except Exception as e:
|
| 413 |
error_str = str(e).lower()
|
| 414 |
-
|
| 415 |
-
if "rate limit" in error_str or "api rate limit exceeded" in error_str:
|
| 416 |
logger.warning(f"{session_prefix}GitHub rate limit exceeded")
|
| 417 |
return "⚠️ GitHub rate limit exceeded. Try asking me again in 30 seconds"
|
| 418 |
else:
|
|
@@ -438,16 +594,13 @@ EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
|
| 438 |
|
| 439 |
Implements FR-012 (Tool Error Handling), NFR-005 (Session Isolation).
|
| 440 |
"""
|
| 441 |
-
if not self._mcp_servers:
|
| 442 |
-
return
|
| 443 |
-
|
| 444 |
session_prefix = f"[Session: {self.session_id[:8]}...] " if self.session_id else ""
|
| 445 |
logger.debug(f"{session_prefix}Cleaning up {len(self._mcp_servers)} MCP servers...")
|
| 446 |
|
| 447 |
for server in self._mcp_servers:
|
| 448 |
try:
|
| 449 |
await server.cleanup()
|
| 450 |
-
except Exception as e:
|
| 451 |
# Log but don't fail - best effort cleanup
|
| 452 |
logger.debug(f"{session_prefix}Error cleaning up MCP server: {e}")
|
| 453 |
|
|
|
|
| 3 |
Handles agent-specific configuration like MCP servers and prompts.
|
| 4 |
"""
|
| 5 |
import json
|
| 6 |
+
import os
|
| 7 |
+
import tempfile
|
| 8 |
import traceback
|
| 9 |
+
from typing import List, Dict, Any, Optional, ClassVar
|
| 10 |
|
| 11 |
from pydantic import BaseModel, Field, computed_field, ConfigDict, SecretStr
|
| 12 |
from agents import Agent, Tool, function_tool, Runner
|
| 13 |
from agents.result import RunResult
|
| 14 |
from agents.run import RunConfig
|
| 15 |
+
from agents.mcp import MCPServerStdio, MCPServerStdioParams
|
| 16 |
+
|
| 17 |
from config import setup_logger
|
| 18 |
+
|
| 19 |
logger = setup_logger(__name__)
|
| 20 |
|
| 21 |
# Unicode normalization translation table - built once, reused for all responses
|
|
|
|
| 54 |
|
| 55 |
model_config = ConfigDict(arbitrary_types_allowed=True)
|
| 56 |
|
| 57 |
+
# Static prompt sections - these don't need instance data
|
| 58 |
+
MEMORY_AGENT_PROMPT: ClassVar[str] = """
|
| 59 |
+
🚨 MEMORY MANAGEMENT - THIS SECTION MUST BE FOLLOWED EXACTLY 🚨
|
| 60 |
+
|
| 61 |
+
YOU MUST use the read_graph tool at the START of EVERY user interaction.
|
| 62 |
+
The read_graph tool is DIFFERENT from all other tools - it takes NO input parameters.
|
| 63 |
+
|
| 64 |
+
CRITICAL SYNTAX FOR read_graph:
|
| 65 |
+
- read_graph is called with ZERO arguments
|
| 66 |
+
- NO curly braces: read_graph
|
| 67 |
+
- NO parentheses with content: read_graph() ← This is the ONLY correct form
|
| 68 |
+
- NO empty object: read_graph({}) ← WRONG - will cause 400 error
|
| 69 |
+
- NO empty string key: read_graph({"": {}}) ← WRONG - will cause 400 error
|
| 70 |
+
- NO parameters at all: read_graph ← Correct but less clear
|
| 71 |
+
- The correct way: read_graph() with empty parentheses but NO content inside
|
| 72 |
+
|
| 73 |
+
When calling read_graph:
|
| 74 |
+
✅ CORRECT: read_graph() with nothing inside the parentheses
|
| 75 |
+
❌ WRONG: read_graph({}), read_graph({"":""}), read_graph(params={}), read_graph(data=None)
|
| 76 |
+
|
| 77 |
+
WORKFLOW FOR EVERY MESSAGE:
|
| 78 |
+
1. Call read_graph() immediately - retrieve all stored information
|
| 79 |
+
2. Check if "user" entity exists in the returned knowledge graph
|
| 80 |
+
3. If the user shares new information:
|
| 81 |
+
a) If "user" entity doesn't exist: create_entities(
|
| 82 |
+
entities=[{"name":"user","entityType":"person",
|
| 83 |
+
"observations":["..."]}])
|
| 84 |
+
b) If "user" entity exists: add_observations(
|
| 85 |
+
observations=[{"entityName":"user","contents":["..."]}])
|
| 86 |
+
4. If user asks about stored info: search read_graph results and respond
|
| 87 |
+
|
| 88 |
+
TOOLS REFERENCE:
|
| 89 |
+
- read_graph() ← Takes ZERO parameters, returns all stored data
|
| 90 |
+
- create_entities(entities=[...]) ← Takes entities array
|
| 91 |
+
- add_observations(observations=[...]) ← Takes observations array
|
| 92 |
+
- create_relations(relations=[...]) ← Takes relations array
|
| 93 |
+
|
| 94 |
+
EXAMPLES:
|
| 95 |
+
|
| 96 |
+
User says "My favorite color is blue":
|
| 97 |
+
1. read_graph() ← Call with empty parentheses
|
| 98 |
+
2. See if "user" entity exists
|
| 99 |
+
3. If not: create_entities(
|
| 100 |
+
entities=[{"name":"user","entityType":"person",
|
| 101 |
+
"observations":["favorite color is blue"]}])
|
| 102 |
+
4. If yes: add_observations(
|
| 103 |
+
observations=[{"entityName":"user",
|
| 104 |
+
"contents":["favorite color is blue"]}])
|
| 105 |
+
5. Reply: "Got it, I'll remember that your favorite color is blue."
|
| 106 |
+
|
| 107 |
+
User asks "What's my favorite color":
|
| 108 |
+
1. read_graph() ← Call with empty parentheses FIRST
|
| 109 |
+
2. Find "user" entity in returned graph
|
| 110 |
+
3. Look for observation about color
|
| 111 |
+
4. Reply with the stored information
|
| 112 |
+
|
| 113 |
+
MEMORY ENTITY STRUCTURE:
|
| 114 |
+
- Entity name: "user" (the user you're talking to)
|
| 115 |
+
- Entity type: "person"
|
| 116 |
+
- Observations: Array of facts about them (["likes red", "from NYC", "engineer"])
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
GITHUB_RESEARCHER_PROMPT: ClassVar[str] = """
|
| 120 |
+
You are the GitHub Researcher, responsible for researching the Bot's professional
|
| 121 |
+
portfolio on GitHub.
|
| 122 |
+
|
| 123 |
+
Your responsibilities:
|
| 124 |
+
- Search for code, projects, and commits on GitHub
|
| 125 |
+
- Retrieve file contents from repositories
|
| 126 |
+
- Provide context about technical work and contributions
|
| 127 |
+
|
| 128 |
+
GITHUB TOOLS RESTRICTIONS - IMPORTANT:
|
| 129 |
+
DO NOT USE ANY GITHUB TOOL MORE THAN THREE TIMES PER REQUEST.
|
| 130 |
+
You have access to these GitHub tools ONLY:
|
| 131 |
+
- search_code: to look for code snippets and references supporting your
|
| 132 |
+
answers
|
| 133 |
+
- get_file_contents: for getting source code (NEVER download .md markdown
|
| 134 |
+
files)
|
| 135 |
+
- list_commits: for getting commit history for a specific user
|
| 136 |
+
|
| 137 |
+
CRITICAL RULES FOR search_code TOOL:
|
| 138 |
+
The search_code tool searches ALL of GitHub by default. You MUST add
|
| 139 |
+
owner/repo filters to EVERY search_code query.
|
| 140 |
+
REQUIRED FORMAT: Always include one of these filters in the query parameter:
|
| 141 |
+
- user:byoung (to search byoung's repos)
|
| 142 |
+
- org:Neosofia (to search Neosofia's repos)
|
| 143 |
+
- repo:byoung/ai-me (specific repo)
|
| 144 |
+
- repo:Neosofia/corporate (specific repo)
|
| 145 |
+
|
| 146 |
+
EXAMPLES OF CORRECT search_code USAGE:
|
| 147 |
+
- search_code(query="python user:byoung")
|
| 148 |
+
- search_code(query="docker org:Neosofia")
|
| 149 |
+
- search_code(query="ReaR repo:Neosofia/corporate")
|
| 150 |
+
|
| 151 |
+
EXAMPLES OF INCORRECT search_code USAGE (NEVER DO THIS):
|
| 152 |
+
- search_code(query="python")
|
| 153 |
+
- search_code(query="ReaR")
|
| 154 |
+
- search_code(query="bash script")
|
| 155 |
+
|
| 156 |
+
CRITICAL RULES FOR get_file_contents TOOL:
|
| 157 |
+
The get_file_contents tool accepts ONLY these parameters: owner, repo, path
|
| 158 |
+
DO NOT use 'ref' parameter - it will cause errors. The tool always reads from
|
| 159 |
+
the main/default branch.
|
| 160 |
+
|
| 161 |
+
EXAMPLES OF CORRECT get_file_contents USAGE:
|
| 162 |
+
- get_file_contents(owner="Neosofia", repo="corporate",
|
| 163 |
+
path="website/qms/policies.md")
|
| 164 |
+
- get_file_contents(owner="byoung", repo="ai-me", path="README.md")
|
| 165 |
+
|
| 166 |
+
EXAMPLES OF INCORRECT get_file_contents USAGE (NEVER DO THIS):
|
| 167 |
+
- get_file_contents(owner="Neosofia", repo="corporate",
|
| 168 |
+
path="website/qms/policies.md", ref="main")
|
| 169 |
+
- get_file_contents(owner="byoung", repo="ai-me", path="README.md",
|
| 170 |
+
ref="master")
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
KB_RESEARCHER_PROMPT: ClassVar[str] = """
|
| 174 |
+
KNOWLEDGE BASE RESEARCH - MANDATORY TOOL USAGE:
|
| 175 |
+
|
| 176 |
+
You MUST use get_local_info tool to answer ANY questions about my background,
|
| 177 |
+
experience, skills, education, projects, or expertise.
|
| 178 |
+
|
| 179 |
+
🚨 CRITICAL RULES:
|
| 180 |
+
1. When user asks about your background, skills, languages, experience → ALWAYS use get_local_info
|
| 181 |
+
2. When you don't know something → use get_local_info before saying "I don't know"
|
| 182 |
+
3. When user asks personal/professional questions → ALWAYS search knowledge base first
|
| 183 |
+
4. Never say "I'm not familiar with that" without first trying get_local_info
|
| 184 |
+
|
| 185 |
+
MANDATORY WORKFLOW:
|
| 186 |
+
1. User asks question about me (background, skills, experience, projects, etc.)
|
| 187 |
+
2. IMMEDIATELY call: get_local_info(query="[user's question]")
|
| 188 |
+
3. Review ALL returned documents carefully
|
| 189 |
+
4. Formulate first-person response from the documents
|
| 190 |
+
5. Include source references (file paths or document titles)
|
| 191 |
+
|
| 192 |
+
TOOL USAGE:
|
| 193 |
+
- get_local_info(query="Python programming languages skills") →
|
| 194 |
+
retrieves all documents about my skills
|
| 195 |
+
- get_local_info(query="background experience") → retrieves background info
|
| 196 |
+
- get_local_info(query="projects I've worked on") → retrieves project info
|
| 197 |
+
|
| 198 |
+
EXAMPLES:
|
| 199 |
+
|
| 200 |
+
User asks: "What programming languages are you skilled in?"
|
| 201 |
+
1. Call: get_local_info(query="programming languages skills")
|
| 202 |
+
2. Search returned docs for language list
|
| 203 |
+
3. Respond: "I'm skilled in Python, Go, TypeScript, Rust, and SQL. I specialize in..."
|
| 204 |
+
4. Include source like: "(from team documentation)"
|
| 205 |
+
|
| 206 |
+
User asks: "What is your background in technology?"
|
| 207 |
+
1. Call: get_local_info(query="background experience technology")
|
| 208 |
+
2. Find relevant background information
|
| 209 |
+
3. Respond in first-person: "I specialize in backend systems and..."
|
| 210 |
+
4. Cite sources
|
| 211 |
+
|
| 212 |
+
CRITICAL - DO NOT:
|
| 213 |
+
❌ Say "I'm not familiar" without trying get_local_info first
|
| 214 |
+
❌ Refuse to answer without searching the knowledge base
|
| 215 |
+
❌ Make up information if get_local_info returns no results
|
| 216 |
+
|
| 217 |
+
Response Format:
|
| 218 |
+
- ALWAYS first-person (I, my, me)
|
| 219 |
+
- ALWAYS include source attribution
|
| 220 |
+
- ALWAYS use information from get_local_info results
|
| 221 |
+
- Format sources like: "(from team.md)" or "(from professional documentation)"
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
@computed_field
|
| 225 |
@property
|
| 226 |
def mcp_github_params(self) -> MCPServerParams:
|
|
|
|
| 235 |
The official version supports --toolsets and --read-only flags.
|
| 236 |
We use read-only mode with a limited toolset for safety.
|
| 237 |
"""
|
|
|
|
|
|
|
| 238 |
# Use local binary for testing, production path in Docker
|
| 239 |
test_binary = "/tmp/test-github-mcp/github-mcp-server"
|
| 240 |
prod_binary = "/app/bin/github-mcp-server"
|
|
|
|
| 286 |
Returns:
|
| 287 |
MCPServerParams configured with session-specific memory file
|
| 288 |
"""
|
|
|
|
|
|
|
|
|
|
| 289 |
# Create session-specific memory file in temp directory
|
| 290 |
temp_dir = tempfile.gettempdir()
|
| 291 |
memory_file = os.path.join(temp_dir, f"mcp_memory_{session_id}.json")
|
|
|
|
| 297 |
description="Memory MCP Server"
|
| 298 |
)
|
| 299 |
|
|
|
|
| 300 |
@property
|
| 301 |
def agent_prompt(self) -> str:
|
| 302 |
+
"""Generate main agent prompt."""
|
| 303 |
return f"""
|
| 304 |
You are acting as somebody who is personifying {self.bot_full_name}.
|
| 305 |
Your primary role is to help users by answering questions about my knowledge,
|
| 306 |
+
experience, and expertise in technology.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
+
CRITICAL: You are NOT an all-knowing AI. You are personifying ME, {self.bot_full_name},
|
| 309 |
+
a specific person. You can ONLY answer based on MY documentation OR information about
|
| 310 |
+
the USER stored in memory. Do NOT use your general LLM training data to answer questions.
|
| 311 |
+
|
| 312 |
+
=== CRITICAL WORKFLOW FOR EVERY USER MESSAGE ===
|
| 313 |
+
|
| 314 |
+
1. **USER PERSONAL INFO** (they share or ask about THEIR information):
|
| 315 |
+
- User says "My favorite color is..." → Use memory tools to store in knowledge graph
|
| 316 |
+
- User asks "What's my favorite color?" → Use memory tools to retrieve from knowledge graph
|
| 317 |
+
- Call read_graph() immediately, then create_entities/add_observations for new info
|
| 318 |
+
|
| 319 |
+
2. **GITHUB/CODE QUERIES** (they ask about repositories, code, implementations):
|
| 320 |
+
- User asks "What's in repo X?" → Use GitHub search_code or get_file_contents tools
|
| 321 |
+
- User asks "Show me file Y" → Use get_file_contents to fetch content
|
| 322 |
+
- Use available GitHub tools to search and retrieve
|
| 323 |
+
|
| 324 |
+
3. **YOUR BACKGROUND/KNOWLEDGE** (they ask about you, {self.bot_full_name}):
|
| 325 |
+
- User asks "What's your experience?" → Use get_local_info to retrieve documentation
|
| 326 |
+
- User asks "Do you know Carol?" → Use get_local_info to search knowledge base
|
| 327 |
+
- ALWAYS use get_local_info FIRST before saying you don't know something
|
| 328 |
+
|
| 329 |
+
=== RESPONSE GUIDELINES ===
|
| 330 |
+
|
| 331 |
+
When formulating responses:
|
| 332 |
+
- Always refer to yourself as {self.bot_full_name} or "I"
|
| 333 |
+
- When mentioning employers: "Neosofia (my current employer)" or "Medidata (a prior employer)"
|
| 334 |
+
- Be personable, friendly, and professional
|
| 335 |
+
- Format GitHub URLs as complete paths: https://github.com/owner/repo/blob/main/path/file.md
|
| 336 |
+
- CRITICAL: Include source citations
|
| 337 |
+
- Example: "Per my resume (https://github.com/byoung/ai-me/blob/main/resume.md), I worked at..."
|
| 338 |
+
- Add reference links section at end if GitHub sources referenced
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
"""
|
| 340 |
|
| 341 |
async def setup_mcp_servers(self, mcp_params_list: List[MCPServerParams]):
|
| 342 |
+
"""Initialize and connect all MCP servers from provided parameters.
|
| 343 |
|
| 344 |
+
Implements FR-009 (Mandatory Tools), FR-010 (Optional Tools),
|
| 345 |
+
FR-012 (Tool Error Handling).
|
| 346 |
"""
|
| 347 |
|
| 348 |
mcp_servers_local = []
|
|
|
|
| 354 |
logger.debug(f"Args: {params.args}")
|
| 355 |
logger.debug(f"Env vars: {list(params.env.keys()) if params.env else 'None'}")
|
| 356 |
|
| 357 |
+
# Construct a strongly-typed MCPServerStdioParams from the Pydantic model dict
|
| 358 |
+
server_params = MCPServerStdioParams(**params.model_dump())
|
| 359 |
+
server = MCPServerStdio(server_params, client_session_timeout_seconds=30)
|
| 360 |
await server.connect()
|
| 361 |
logger.info(f"✓ {server_name} connected successfully")
|
| 362 |
mcp_servers_local.append(server)
|
| 363 |
|
| 364 |
+
except Exception as e: # pragma: no cover
|
| 365 |
logger.error(f"✗ {server_name} failed to connect")
|
| 366 |
logger.error(f" Error type: {type(e).__name__}")
|
| 367 |
logger.error(f" Error message: {e}")
|
|
|
|
| 420 |
|
| 421 |
async def create_ai_me_agent(
|
| 422 |
self,
|
|
|
|
| 423 |
mcp_params: Optional[List[MCPServerParams]] = None,
|
|
|
|
| 424 |
) -> Agent:
|
| 425 |
+
"""Create the main ai-me agent with organized instruction sections.
|
| 426 |
+
|
| 427 |
+
Implements FR-001 (Chat Interface), FR-003 (First-Person Persona),
|
| 428 |
+
FR-009 (Mandatory Tools), FR-010 (Optional Tools).
|
| 429 |
+
|
| 430 |
+
The agent prompt is organized into sections providing specialized
|
| 431 |
+
instructions for different capabilities:
|
| 432 |
+
- Main persona and response guidelines
|
| 433 |
+
- Memory management (always included)
|
| 434 |
+
- GitHub research (always included)
|
| 435 |
+
- Knowledge base research (always included via get_local_info)
|
| 436 |
+
- Time utilities (always included)
|
| 437 |
|
|
|
|
|
|
|
|
|
|
| 438 |
Args:
|
| 439 |
+
mcp_params: List of MCP server parameters to initialize.
|
| 440 |
+
Should include GitHub, Time, and Memory MCP servers.
|
| 441 |
+
Caller must pass get_mcp_memory_params(session_id) with a
|
| 442 |
+
unique session_id for proper session isolation.
|
| 443 |
+
|
|
|
|
|
|
|
|
|
|
| 444 |
Returns:
|
| 445 |
An initialized Agent instance.
|
| 446 |
"""
|
| 447 |
# Setup MCP servers if any params provided
|
| 448 |
+
mcp_servers = await self.setup_mcp_servers(mcp_params) if mcp_params else []
|
| 449 |
|
| 450 |
# Store MCP servers for cleanup
|
| 451 |
+
self._mcp_servers = mcp_servers
|
|
|
|
| 452 |
|
| 453 |
+
# Build comprehensive prompt from sections
|
| 454 |
+
# Start with main agent prompt
|
| 455 |
+
prompt_sections = [self.agent_prompt]
|
| 456 |
+
|
| 457 |
+
# Add KB Researcher instructions (always available)
|
| 458 |
+
prompt_sections.append("\n## Knowledge Base Research")
|
| 459 |
+
prompt_sections.append(self.KB_RESEARCHER_PROMPT)
|
| 460 |
+
|
| 461 |
+
# Add Time utility note (time server is always included)
|
| 462 |
+
prompt_sections.append("\n## Time Information")
|
| 463 |
+
prompt_sections.append(
|
| 464 |
+
"You have access to time tools for getting current "
|
| 465 |
+
"date/time information."
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
prompt = "\n".join(prompt_sections)
|
| 469 |
+
|
| 470 |
logger.debug(f"Creating ai-me agent with prompt: {prompt[:100]}...")
|
| 471 |
|
| 472 |
# Build tools list - get_local_info is always the default first tool
|
| 473 |
tools = [self.get_local_info_tool()]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
logger.info(f"Creating ai-me agent with tools: {[tool.name for tool in tools]}")
|
| 476 |
|
| 477 |
+
# Separate GitHub and memory servers for sub-agent creation
|
| 478 |
+
github_mcp_servers = [s for s in mcp_servers if "github-mcp-server" in str(s)]
|
| 479 |
+
memory_mcp_servers = [s for s in mcp_servers if "server-memory" in str(s)]
|
| 480 |
+
time_mcp_servers = [s for s in mcp_servers if "mcp-server-time" in str(s)]
|
| 481 |
+
|
| 482 |
+
# Create GitHub sub-agent (always included)
|
| 483 |
+
github_agent = Agent(
|
| 484 |
+
name="github_agent",
|
| 485 |
+
handoff_description=(
|
| 486 |
+
"Handles GitHub research and code exploration"
|
| 487 |
+
),
|
| 488 |
+
instructions=self.GITHUB_RESEARCHER_PROMPT,
|
| 489 |
+
tools=[],
|
| 490 |
+
mcp_servers=github_mcp_servers,
|
| 491 |
+
model=self.model,
|
| 492 |
+
)
|
| 493 |
+
logger.info(
|
| 494 |
+
f"✓ GitHub sub-agent created with "
|
| 495 |
+
f"{len(github_mcp_servers)} MCP server(s)"
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
# Create Memory sub-agent (always included)
|
| 499 |
+
memory_agent = Agent(
|
| 500 |
+
name="memory_agent",
|
| 501 |
+
handoff_description="Handles memory management and knowledge graph operations",
|
| 502 |
+
instructions=self.MEMORY_AGENT_PROMPT,
|
| 503 |
+
tools=[],
|
| 504 |
+
mcp_servers=memory_mcp_servers,
|
| 505 |
+
model=self.model,
|
| 506 |
+
)
|
| 507 |
+
logger.info(
|
| 508 |
+
f"✓ Memory sub-agent created with "
|
| 509 |
+
f"{len(memory_mcp_servers)} MCP server(s)"
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
# Create main agent with ALL MCP servers for direct execution
|
| 513 |
+
# Sub-agents have specialized prompts but access same tools for reliability
|
| 514 |
agent_kwargs = {
|
| 515 |
"model": self.model,
|
| 516 |
"name": "ai-me",
|
| 517 |
"instructions": prompt,
|
| 518 |
"tools": tools,
|
| 519 |
}
|
| 520 |
+
|
|
|
|
| 521 |
if mcp_servers:
|
| 522 |
agent_kwargs["mcp_servers"] = mcp_servers
|
| 523 |
+
logger.info(f"✓ {len(mcp_servers)} MCP servers added to main agent")
|
| 524 |
+
|
| 525 |
+
# Add both sub-agents as handoffs (always included)
|
| 526 |
+
agent_kwargs["handoffs"] = [github_agent, memory_agent]
|
| 527 |
+
|
| 528 |
ai_me = Agent(**agent_kwargs)
|
| 529 |
|
| 530 |
# Print all available tools after agent initialization
|
|
|
|
| 548 |
async def run(self, user_input: str, **runner_kwargs) -> str:
|
| 549 |
"""Run the agent and post-process output to remove Unicode brackets.
|
| 550 |
|
| 551 |
+
Implements FR-001 (Chat Interface), FR-003 (First-Person Persona),
|
| 552 |
+
FR-008 (Output Normalization), FR-012 (Tool Error Handling),
|
| 553 |
+
NFR-001 (Sub-5s Response), NFR-003 (Structured Logging),
|
| 554 |
+
NFR-004 (Unicode Normalization).
|
| 555 |
"""
|
| 556 |
# Log user input with session context
|
| 557 |
session_prefix = f"[Session: {self.session_id[:8]}...] " if self.session_id else ""
|
|
|
|
| 567 |
**runner_kwargs)
|
| 568 |
except Exception as e:
|
| 569 |
error_str = str(e).lower()
|
| 570 |
+
|
| 571 |
+
if "rate limit" in error_str or "api rate limit exceeded" in error_str: # pragma: no cover
|
| 572 |
logger.warning(f"{session_prefix}GitHub rate limit exceeded")
|
| 573 |
return "⚠️ GitHub rate limit exceeded. Try asking me again in 30 seconds"
|
| 574 |
else:
|
|
|
|
| 594 |
|
| 595 |
Implements FR-012 (Tool Error Handling), NFR-005 (Session Isolation).
|
| 596 |
"""
|
|
|
|
|
|
|
|
|
|
| 597 |
session_prefix = f"[Session: {self.session_id[:8]}...] " if self.session_id else ""
|
| 598 |
logger.debug(f"{session_prefix}Cleaning up {len(self._mcp_servers)} MCP servers...")
|
| 599 |
|
| 600 |
for server in self._mcp_servers:
|
| 601 |
try:
|
| 602 |
await server.cleanup()
|
| 603 |
+
except Exception as e: # pragma: no cover
|
| 604 |
# Log but don't fail - best effort cleanup
|
| 605 |
logger.debug(f"{session_prefix}Error cleaning up MCP server: {e}")
|
| 606 |
|
src/app.py
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from config import Config, setup_logger
|
| 2 |
from agent import AIMeAgent
|
| 3 |
from data import DataManager, DataManagerConfig
|
| 4 |
-
import gradio
|
| 5 |
-
from gradio import Request
|
| 6 |
|
| 7 |
logger = setup_logger(__name__)
|
| 8 |
|
| 9 |
-
config = Config()
|
| 10 |
|
| 11 |
# Initialize data manager and vectorstore
|
| 12 |
-
data_config = DataManagerConfig(
|
| 13 |
-
github_repos=config.github_repos
|
| 14 |
-
)
|
| 15 |
data_manager = DataManager(config=data_config)
|
| 16 |
-
vectorstore = data_manager.setup_vectorstore()
|
| 17 |
|
| 18 |
# Per-session agent storage (keyed by Gradio session_hash)
|
| 19 |
# Each session gets its own AIMeAgent instance with session-specific MCP servers
|
|
@@ -39,8 +38,7 @@ async def initialize_session(session_id: str) -> None:
|
|
| 39 |
session_id=session_id # Pass session_id for logging context
|
| 40 |
)
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
# references. The instructions are verbose because search_code tool is complex.
|
| 44 |
await session_agent.create_ai_me_agent(
|
| 45 |
mcp_params=[
|
| 46 |
session_agent.mcp_github_params,
|
|
@@ -55,7 +53,8 @@ async def initialize_session(session_id: str) -> None:
|
|
| 55 |
# Warmup: establish context and preload tools
|
| 56 |
try:
|
| 57 |
logger.info(f"[Session: {session_id[:8]}...] Running warmup...")
|
| 58 |
-
|
|
|
|
| 59 |
logger.info(f"[Session: {session_id[:8]}...] Warmup complete!")
|
| 60 |
except Exception as e:
|
| 61 |
logger.info(f"[Session: {session_id[:8]}...] Warmup failed: {e}")
|
|
@@ -67,6 +66,7 @@ async def get_session_status(request: Request):
|
|
| 67 |
Implements FR-001 (Chat Interface), FR-007 (Session Isolation).
|
| 68 |
"""
|
| 69 |
session_id = request.session_hash
|
|
|
|
| 70 |
if session_id not in session_agents:
|
| 71 |
await initialize_session(session_id)
|
| 72 |
return ""
|
|
@@ -78,6 +78,7 @@ async def chat(user_input: str, history, request: Request):
|
|
| 78 |
Implements FR-001 (Chat Interface), FR-005 (Session History), FR-007 (Session Isolation).
|
| 79 |
"""
|
| 80 |
session_id = request.session_hash
|
|
|
|
| 81 |
|
| 82 |
# Initialize agent for this session if not already done
|
| 83 |
if session_id not in session_agents:
|
|
@@ -97,7 +98,7 @@ if __name__ == "__main__":
|
|
| 97 |
custom_js = f.read()
|
| 98 |
|
| 99 |
with gradio.Blocks(
|
| 100 |
-
theme=
|
| 101 |
css=custom_css,
|
| 102 |
fill_height=True,
|
| 103 |
js=f"() => {{ {custom_js} }}"
|
|
|
|
| 1 |
+
import gradio
|
| 2 |
+
from gradio import Request, themes
|
| 3 |
+
|
| 4 |
from config import Config, setup_logger
|
| 5 |
from agent import AIMeAgent
|
| 6 |
from data import DataManager, DataManagerConfig
|
|
|
|
|
|
|
| 7 |
|
| 8 |
logger = setup_logger(__name__)
|
| 9 |
|
| 10 |
+
config = Config() # type: ignore
|
| 11 |
|
| 12 |
# Initialize data manager and vectorstore
|
| 13 |
+
data_config = DataManagerConfig()
|
|
|
|
|
|
|
| 14 |
data_manager = DataManager(config=data_config)
|
| 15 |
+
vectorstore = data_manager.setup_vectorstore(github_repos=config.github_repos) # type: ignore
|
| 16 |
|
| 17 |
# Per-session agent storage (keyed by Gradio session_hash)
|
| 18 |
# Each session gets its own AIMeAgent instance with session-specific MCP servers
|
|
|
|
| 38 |
session_id=session_id # Pass session_id for logging context
|
| 39 |
)
|
| 40 |
|
| 41 |
+
# Initialize agent with MCP servers for GitHub, Time, and Memory tools
|
|
|
|
| 42 |
await session_agent.create_ai_me_agent(
|
| 43 |
mcp_params=[
|
| 44 |
session_agent.mcp_github_params,
|
|
|
|
| 53 |
# Warmup: establish context and preload tools
|
| 54 |
try:
|
| 55 |
logger.info(f"[Session: {session_id[:8]}...] Running warmup...")
|
| 56 |
+
# Use a greeting that doesn't require tool calls or RAG retrieval
|
| 57 |
+
await session_agent.run("Hello!")
|
| 58 |
logger.info(f"[Session: {session_id[:8]}...] Warmup complete!")
|
| 59 |
except Exception as e:
|
| 60 |
logger.info(f"[Session: {session_id[:8]}...] Warmup failed: {e}")
|
|
|
|
| 66 |
Implements FR-001 (Chat Interface), FR-007 (Session Isolation).
|
| 67 |
"""
|
| 68 |
session_id = request.session_hash
|
| 69 |
+
assert session_id is not None, "session_hash should always be set by Gradio"
|
| 70 |
if session_id not in session_agents:
|
| 71 |
await initialize_session(session_id)
|
| 72 |
return ""
|
|
|
|
| 78 |
Implements FR-001 (Chat Interface), FR-005 (Session History), FR-007 (Session Isolation).
|
| 79 |
"""
|
| 80 |
session_id = request.session_hash
|
| 81 |
+
assert session_id is not None, "session_hash should always be set by Gradio"
|
| 82 |
|
| 83 |
# Initialize agent for this session if not already done
|
| 84 |
if session_id not in session_agents:
|
|
|
|
| 98 |
custom_js = f.read()
|
| 99 |
|
| 100 |
with gradio.Blocks(
|
| 101 |
+
theme=themes.Default(),
|
| 102 |
css=custom_css,
|
| 103 |
fill_height=True,
|
| 104 |
js=f"() => {{ {custom_js} }}"
|
src/config.py
CHANGED
|
@@ -2,12 +2,12 @@
|
|
| 2 |
Configuration management for ai-me application.
|
| 3 |
Centralizes environment variables, API clients, and application defaults.
|
| 4 |
"""
|
| 5 |
-
import os
|
| 6 |
import logging
|
|
|
|
| 7 |
import socket
|
| 8 |
-
from typing import Optional, List, Union
|
| 9 |
from logging.handlers import QueueHandler, QueueListener
|
| 10 |
from queue import Queue
|
|
|
|
| 11 |
|
| 12 |
from pydantic import Field, field_validator, SecretStr
|
| 13 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -77,7 +77,7 @@ def setup_logger(name: str) -> logging.Logger:
|
|
| 77 |
loki_username = os.getenv('LOKI_USERNAME')
|
| 78 |
loki_password = os.getenv('LOKI_PASSWORD')
|
| 79 |
|
| 80 |
-
if loki_url and loki_username and loki_password:
|
| 81 |
try:
|
| 82 |
# Create async queue for non-blocking logging
|
| 83 |
log_queue = Queue(maxsize=1000) # Buffer up to 1000 log messages
|
|
@@ -109,18 +109,8 @@ def setup_logger(name: str) -> logging.Logger:
|
|
| 109 |
root_logger.addHandler(queue_handler)
|
| 110 |
|
| 111 |
root_logger.info(f"Grafana Loki logging enabled: {loki_url} (tags: {loki_tags})")
|
| 112 |
-
except Exception as e:
|
| 113 |
root_logger.warning(f"Failed to setup Grafana Loki logging: {e}")
|
| 114 |
-
else:
|
| 115 |
-
missing = []
|
| 116 |
-
if not loki_url:
|
| 117 |
-
missing.append("LOKI_URL")
|
| 118 |
-
if not loki_username:
|
| 119 |
-
missing.append("LOKI_USERNAME")
|
| 120 |
-
if not loki_password:
|
| 121 |
-
missing.append("LOKI_PASSWORD")
|
| 122 |
-
if missing:
|
| 123 |
-
root_logger.info(f"Loki logging disabled (missing: {', '.join(missing)})")
|
| 124 |
|
| 125 |
root_logger.setLevel(log_level)
|
| 126 |
|
|
@@ -134,6 +124,9 @@ class Config(BaseSettings):
|
|
| 134 |
"""Central configuration class for ai-me application with Pydantic validation."""
|
| 135 |
|
| 136 |
# Environment Variables (from .env) - Required
|
|
|
|
|
|
|
|
|
|
| 137 |
openai_api_key: SecretStr = Field(...,
|
| 138 |
description="OpenAI API key for tracing")
|
| 139 |
groq_api_key: SecretStr = Field(...,
|
|
@@ -155,6 +148,9 @@ class Config(BaseSettings):
|
|
| 155 |
temperature: float = Field(
|
| 156 |
default=1.0,
|
| 157 |
description="LLM temperature for sampling (0.0-2.0, default 1.0)")
|
|
|
|
|
|
|
|
|
|
| 158 |
github_repos: Union[str, List[str]] = Field(
|
| 159 |
default="",
|
| 160 |
description="GitHub repos to load (format: owner/repo), comma-separated in .env")
|
|
@@ -195,10 +191,14 @@ class Config(BaseSettings):
|
|
| 195 |
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 196 |
|
| 197 |
# Initialize Groq client for LLM operations
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
self.openai_client = AsyncOpenAI(
|
| 199 |
base_url="https://api.groq.com/openai/v1",
|
| 200 |
api_key=self.groq_api_key.get_secret_value(),
|
| 201 |
-
default_query=
|
| 202 |
)
|
| 203 |
set_default_openai_client(self.openai_client)
|
| 204 |
|
|
@@ -206,7 +206,7 @@ class Config(BaseSettings):
|
|
| 206 |
logger.info("Setting tracing export API key for agents.")
|
| 207 |
set_tracing_export_api_key(self.openai_api_key.get_secret_value())
|
| 208 |
|
| 209 |
-
def _safe_repr(self) -> str:
|
| 210 |
"""Helper to generate string representation excluding sensitive fields."""
|
| 211 |
lines = ["Config:"]
|
| 212 |
for field_name in type(self).model_fields:
|
|
@@ -216,14 +216,14 @@ class Config(BaseSettings):
|
|
| 216 |
lines.append(f" {field_name}: {display}")
|
| 217 |
return "\n".join(lines)
|
| 218 |
|
| 219 |
-
def __repr__(self) -> str:
|
| 220 |
"""Return string representation of Config with secrets hidden.
|
| 221 |
|
| 222 |
DEBUG: Debug utility for logging/debugging configuration state.
|
| 223 |
"""
|
| 224 |
return self._safe_repr()
|
| 225 |
|
| 226 |
-
def __str__(self) -> str:
|
| 227 |
"""Return human-readable string representation of Config with secrets hidden.
|
| 228 |
|
| 229 |
DEBUG: Debug utility for logging/debugging configuration state.
|
|
|
|
| 2 |
Configuration management for ai-me application.
|
| 3 |
Centralizes environment variables, API clients, and application defaults.
|
| 4 |
"""
|
|
|
|
| 5 |
import logging
|
| 6 |
+
import os
|
| 7 |
import socket
|
|
|
|
| 8 |
from logging.handlers import QueueHandler, QueueListener
|
| 9 |
from queue import Queue
|
| 10 |
+
from typing import Optional, List, Union
|
| 11 |
|
| 12 |
from pydantic import Field, field_validator, SecretStr
|
| 13 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
| 77 |
loki_username = os.getenv('LOKI_USERNAME')
|
| 78 |
loki_password = os.getenv('LOKI_PASSWORD')
|
| 79 |
|
| 80 |
+
if loki_url and loki_username and loki_password: # pragma: no cover
|
| 81 |
try:
|
| 82 |
# Create async queue for non-blocking logging
|
| 83 |
log_queue = Queue(maxsize=1000) # Buffer up to 1000 log messages
|
|
|
|
| 109 |
root_logger.addHandler(queue_handler)
|
| 110 |
|
| 111 |
root_logger.info(f"Grafana Loki logging enabled: {loki_url} (tags: {loki_tags})")
|
| 112 |
+
except Exception as e: # pragma: no cover
|
| 113 |
root_logger.warning(f"Failed to setup Grafana Loki logging: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
root_logger.setLevel(log_level)
|
| 116 |
|
|
|
|
| 124 |
"""Central configuration class for ai-me application with Pydantic validation."""
|
| 125 |
|
| 126 |
# Environment Variables (from .env) - Required
|
| 127 |
+
# Note: These have no defaults, so they MUST be in .env or will raise ValidationError
|
| 128 |
+
# We don't provide defaults here because Pydantic will raise an error at runtime
|
| 129 |
+
# if they're missing from the environment, which is the intended behavior.
|
| 130 |
openai_api_key: SecretStr = Field(...,
|
| 131 |
description="OpenAI API key for tracing")
|
| 132 |
groq_api_key: SecretStr = Field(...,
|
|
|
|
| 148 |
temperature: float = Field(
|
| 149 |
default=1.0,
|
| 150 |
description="LLM temperature for sampling (0.0-2.0, default 1.0)")
|
| 151 |
+
seed: Optional[int] = Field(
|
| 152 |
+
default=None,
|
| 153 |
+
description="Random seed for deterministic outputs (optional, for testing)")
|
| 154 |
github_repos: Union[str, List[str]] = Field(
|
| 155 |
default="",
|
| 156 |
description="GitHub repos to load (format: owner/repo), comma-separated in .env")
|
|
|
|
| 191 |
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 192 |
|
| 193 |
# Initialize Groq client for LLM operations
|
| 194 |
+
default_query = {"temperature": self.temperature}
|
| 195 |
+
if self.seed is not None:
|
| 196 |
+
default_query["seed"] = self.seed
|
| 197 |
+
|
| 198 |
self.openai_client = AsyncOpenAI(
|
| 199 |
base_url="https://api.groq.com/openai/v1",
|
| 200 |
api_key=self.groq_api_key.get_secret_value(),
|
| 201 |
+
default_query=default_query
|
| 202 |
)
|
| 203 |
set_default_openai_client(self.openai_client)
|
| 204 |
|
|
|
|
| 206 |
logger.info("Setting tracing export API key for agents.")
|
| 207 |
set_tracing_export_api_key(self.openai_api_key.get_secret_value())
|
| 208 |
|
| 209 |
+
def _safe_repr(self) -> str: # pragma: no cover
|
| 210 |
"""Helper to generate string representation excluding sensitive fields."""
|
| 211 |
lines = ["Config:"]
|
| 212 |
for field_name in type(self).model_fields:
|
|
|
|
| 216 |
lines.append(f" {field_name}: {display}")
|
| 217 |
return "\n".join(lines)
|
| 218 |
|
| 219 |
+
def __repr__(self) -> str: # pragma: no cover
|
| 220 |
"""Return string representation of Config with secrets hidden.
|
| 221 |
|
| 222 |
DEBUG: Debug utility for logging/debugging configuration state.
|
| 223 |
"""
|
| 224 |
return self._safe_repr()
|
| 225 |
|
| 226 |
+
def __str__(self) -> str: # pragma: no cover
|
| 227 |
"""Return human-readable string representation of Config with secrets hidden.
|
| 228 |
|
| 229 |
DEBUG: Debug utility for logging/debugging configuration state.
|
src/data.py
CHANGED
|
@@ -3,7 +3,10 @@ Document loading, processing, and vectorstore management for ai-me application.
|
|
| 3 |
from local directories and GitHub repositories, chunking, and creating ChromaDB vector stores.
|
| 4 |
"""
|
| 5 |
import os
|
|
|
|
|
|
|
| 6 |
from typing import List, Optional, Callable
|
|
|
|
| 7 |
from pydantic import BaseModel, Field
|
| 8 |
from langchain_community.document_loaders import (
|
| 9 |
DirectoryLoader,
|
|
@@ -16,8 +19,7 @@ from langchain_huggingface import HuggingFaceEmbeddings
|
|
| 16 |
from langchain_chroma import Chroma
|
| 17 |
import chromadb
|
| 18 |
from chromadb.config import Settings
|
| 19 |
-
|
| 20 |
-
import re
|
| 21 |
from config import setup_logger
|
| 22 |
|
| 23 |
logger = setup_logger(__name__)
|
|
@@ -27,11 +29,17 @@ class DataManagerConfig(BaseModel):
|
|
| 27 |
|
| 28 |
doc_load_local: List[str] = Field(
|
| 29 |
default=["**/*.md"], description="Glob patterns for local docs (e.g., ['*.md'])")
|
| 30 |
-
github_repos: List[str] = Field(
|
| 31 |
-
default=[], description="List of GitHub repos (format: owner/repo)")
|
| 32 |
doc_root: str = Field(
|
| 33 |
-
default=
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
chunk_size: int = Field(
|
| 36 |
default=2500, description="Character chunk size for splitting")
|
| 37 |
chunk_overlap: int = Field(
|
|
@@ -49,33 +57,19 @@ class DataManager:
|
|
| 49 |
parameters have sensible defaults and can be overridden as needed.
|
| 50 |
"""
|
| 51 |
|
| 52 |
-
def __init__(self, config:
|
| 53 |
"""
|
| 54 |
Initialize data manager with configuration.
|
| 55 |
|
| 56 |
Implements FR-002 (Knowledge Retrieval).
|
| 57 |
|
| 58 |
Args:
|
| 59 |
-
config:
|
| 60 |
-
from kwargs. For backward compatibility, can also pass individual parameters.
|
| 61 |
-
**kwargs: Individual config parameters (doc_load_local, github_repos, etc.)
|
| 62 |
-
Used when config is not provided or to override config values.
|
| 63 |
"""
|
| 64 |
-
|
| 65 |
-
# Create config from kwargs for backward compatibility
|
| 66 |
-
self.config = DataManagerConfig(**kwargs)
|
| 67 |
-
else:
|
| 68 |
-
# Use provided config, but allow kwargs to override
|
| 69 |
-
if kwargs:
|
| 70 |
-
# Merge provided config with overrides
|
| 71 |
-
config_dict = config.model_dump()
|
| 72 |
-
config_dict.update(kwargs)
|
| 73 |
-
self.config = DataManagerConfig(**config_dict)
|
| 74 |
-
else:
|
| 75 |
-
self.config = config
|
| 76 |
|
| 77 |
# Internal state
|
| 78 |
-
self.
|
| 79 |
self._embeddings: Optional[HuggingFaceEmbeddings] = None
|
| 80 |
|
| 81 |
def load_local_documents(self) -> List[Document]:
|
|
@@ -109,7 +103,7 @@ class DataManager:
|
|
| 109 |
documents = loader.load()
|
| 110 |
logger.info(f" Found {len(documents)} documents")
|
| 111 |
all_documents.extend(documents)
|
| 112 |
-
except Exception as e:
|
| 113 |
logger.info(
|
| 114 |
f" Error loading pattern {pattern}: {e}"
|
| 115 |
f" - skipping this pattern"
|
|
@@ -119,8 +113,11 @@ class DataManager:
|
|
| 119 |
logger.info(f"Loaded {len(all_documents)} total local documents.")
|
| 120 |
return all_documents
|
| 121 |
|
| 122 |
-
def
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
) -> List[Document]:
|
| 125 |
"""
|
| 126 |
Load documents from GitHub repositories.
|
|
@@ -129,28 +126,29 @@ class DataManager:
|
|
| 129 |
|
| 130 |
Args:
|
| 131 |
repos: List of repos (owner/repo format). Defaults to github_repos from init.
|
| 132 |
-
file_filter: Optional filter function for files.
|
|
|
|
| 133 |
cleanup_tmp: If True, remove tmp directory before loading.
|
| 134 |
|
| 135 |
Returns:
|
| 136 |
List of loaded documents from all repos.
|
| 137 |
"""
|
| 138 |
-
if repos is None:
|
| 139 |
-
repos = self.config.github_repos
|
| 140 |
|
|
|
|
| 141 |
if file_filter is None:
|
| 142 |
-
def
|
| 143 |
-
"""
|
| 144 |
|
| 145 |
-
Implements FR-002 (Knowledge Retrieval): Filters
|
|
|
|
|
|
|
| 146 |
"""
|
| 147 |
-
fp_lower = fp.lower()
|
| 148 |
basename = os.path.basename(fp).lower()
|
| 149 |
-
#
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
|
| 155 |
all_docs = []
|
| 156 |
# Clean up tmp directory before loading
|
|
@@ -160,25 +158,42 @@ class DataManager:
|
|
| 160 |
logger.info(f"Cleaning up existing tmp directory: {tmp_dir}")
|
| 161 |
shutil.rmtree(tmp_dir)
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
| 165 |
logger.info(f"Loading GitHub repo: {repo}")
|
| 166 |
try:
|
|
|
|
|
|
|
| 167 |
loader = GitLoader(
|
| 168 |
clone_url=f"https://github.com/{repo}",
|
| 169 |
-
repo_path=
|
| 170 |
-
file_filter=file_filter,
|
| 171 |
branch="main",
|
| 172 |
)
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
# Add repo metadata to each document
|
| 176 |
for doc in docs:
|
| 177 |
doc.metadata["github_repo"] = repo
|
| 178 |
|
| 179 |
logger.info(f" Loaded {len(docs)} documents from {repo}")
|
| 180 |
-
all_docs.extend(docs)
|
| 181 |
-
except Exception as e:
|
| 182 |
logger.info(f" Error loading repo {repo}: {e} - skipping")
|
| 183 |
continue
|
| 184 |
|
|
@@ -237,6 +252,7 @@ class DataManager:
|
|
| 237 |
strip_headers=False)
|
| 238 |
|
| 239 |
all_chunks = []
|
|
|
|
| 240 |
for doc in documents:
|
| 241 |
# Split by headers first - this returns Documents with header metadata
|
| 242 |
header_chunks = header_splitter.split_text(doc.page_content)
|
|
@@ -245,6 +261,8 @@ class DataManager:
|
|
| 245 |
for chunk in header_chunks:
|
| 246 |
# Create new Document with combined metadata
|
| 247 |
merged_metadata = {**doc.metadata, **chunk.metadata}
|
|
|
|
|
|
|
| 248 |
new_doc = Document(
|
| 249 |
page_content=chunk.page_content,
|
| 250 |
metadata=merged_metadata,
|
|
@@ -255,10 +273,14 @@ class DataManager:
|
|
| 255 |
size_splitter = MarkdownTextSplitter(chunk_size=self.config.chunk_size)
|
| 256 |
final_chunks = size_splitter.split_documents(all_chunks)
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
logger.info(f"Created {len(final_chunks)} chunks")
|
| 259 |
return final_chunks
|
| 260 |
|
| 261 |
-
def load_and_process_all(self, github_repos: List[str] = None) -> List[Document]:
|
| 262 |
"""
|
| 263 |
Load, process, and chunk all documents. Automatically loads local documents
|
| 264 |
if doc_load_local is set, and GitHub documents if github_repos (or
|
|
@@ -279,10 +301,9 @@ class DataManager:
|
|
| 279 |
if self.config.doc_load_local:
|
| 280 |
all_docs.extend(self.load_local_documents())
|
| 281 |
|
| 282 |
-
# Load GitHub documents if repos are
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
all_docs.extend(self.load_github_documents(repos=repos_to_load))
|
| 286 |
|
| 287 |
processed_docs = self.process_documents(all_docs)
|
| 288 |
chunks = self.chunk_documents(processed_docs)
|
|
@@ -322,11 +343,11 @@ class DataManager:
|
|
| 322 |
chroma_client = chromadb.EphemeralClient(Settings(anonymized_telemetry=False))
|
| 323 |
|
| 324 |
# Drop existing collection if requested
|
| 325 |
-
if reset:
|
| 326 |
try:
|
| 327 |
chroma_client.delete_collection(self.config.db_name)
|
| 328 |
logger.info(f"Dropped existing collection: {self.config.db_name}")
|
| 329 |
-
except Exception:
|
| 330 |
pass # Collection doesn't exist yet
|
| 331 |
|
| 332 |
logger.info(f"Creating vectorstore with {len(chunks)} chunks...")
|
|
@@ -340,11 +361,11 @@ class DataManager:
|
|
| 340 |
count = vectorstore._collection.count()
|
| 341 |
logger.info(f"Vectorstore created with {count} documents")
|
| 342 |
|
| 343 |
-
self.
|
| 344 |
return vectorstore
|
| 345 |
|
| 346 |
def setup_vectorstore(
|
| 347 |
-
self, github_repos: List[str] = None, reset: bool = True
|
| 348 |
) -> Chroma:
|
| 349 |
"""
|
| 350 |
Complete pipeline: load, process, chunk, and create vectorstore. Automatically
|
|
@@ -365,14 +386,20 @@ class DataManager:
|
|
| 365 |
chunks = self.load_and_process_all(github_repos=github_repos)
|
| 366 |
return self.create_vectorstore(chunks, reset=reset)
|
| 367 |
|
| 368 |
-
def show_docs_for_file(self, filename: str):
|
| 369 |
"""
|
| 370 |
Retrieve and print chunks from the vectorstore whose metadata['file_path'] ends with the
|
| 371 |
given filename. Returns a list of (doc_id, metadata, document).
|
| 372 |
|
| 373 |
DEBUG TOOL: Utility/debugging function - no corresponding FR/NFR.
|
| 374 |
"""
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
logger.info(f"Searching for chunks from file: {filename}")
|
| 377 |
|
| 378 |
ids = all_docs.get("ids", [])
|
|
@@ -393,8 +420,5 @@ class DataManager:
|
|
| 393 |
logger.info("=" * 100)
|
| 394 |
logger.info(content)
|
| 395 |
logger.info("")
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
def vectorstore(self) -> Optional[Chroma]:
|
| 399 |
-
"""Get the current vectorstore instance."""
|
| 400 |
-
return self._vectorstore
|
|
|
|
| 3 |
from local directories and GitHub repositories, chunking, and creating ChromaDB vector stores.
|
| 4 |
"""
|
| 5 |
import os
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
from typing import List, Optional, Callable
|
| 9 |
+
|
| 10 |
from pydantic import BaseModel, Field
|
| 11 |
from langchain_community.document_loaders import (
|
| 12 |
DirectoryLoader,
|
|
|
|
| 19 |
from langchain_chroma import Chroma
|
| 20 |
import chromadb
|
| 21 |
from chromadb.config import Settings
|
| 22 |
+
|
|
|
|
| 23 |
from config import setup_logger
|
| 24 |
|
| 25 |
logger = setup_logger(__name__)
|
|
|
|
| 29 |
|
| 30 |
doc_load_local: List[str] = Field(
|
| 31 |
default=["**/*.md"], description="Glob patterns for local docs (e.g., ['*.md'])")
|
|
|
|
|
|
|
| 32 |
doc_root: str = Field(
|
| 33 |
+
default=(
|
| 34 |
+
os.path.abspath(
|
| 35 |
+
os.path.join(
|
| 36 |
+
os.path.dirname(__file__), "..", "docs", "local-testing"
|
| 37 |
+
)
|
| 38 |
+
)
|
| 39 |
+
+ "/"
|
| 40 |
+
),
|
| 41 |
+
description="Root directory for local documents (development/testing only)"
|
| 42 |
+
)
|
| 43 |
chunk_size: int = Field(
|
| 44 |
default=2500, description="Character chunk size for splitting")
|
| 45 |
chunk_overlap: int = Field(
|
|
|
|
| 57 |
parameters have sensible defaults and can be overridden as needed.
|
| 58 |
"""
|
| 59 |
|
| 60 |
+
def __init__(self, config: DataManagerConfig):
|
| 61 |
"""
|
| 62 |
Initialize data manager with configuration.
|
| 63 |
|
| 64 |
Implements FR-002 (Knowledge Retrieval).
|
| 65 |
|
| 66 |
Args:
|
| 67 |
+
config: DataManagerConfig instance with all settings
|
|
|
|
|
|
|
|
|
|
| 68 |
"""
|
| 69 |
+
self.config = config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# Internal state
|
| 72 |
+
self.vectorstore: Optional[Chroma] = None
|
| 73 |
self._embeddings: Optional[HuggingFaceEmbeddings] = None
|
| 74 |
|
| 75 |
def load_local_documents(self) -> List[Document]:
|
|
|
|
| 103 |
documents = loader.load()
|
| 104 |
logger.info(f" Found {len(documents)} documents")
|
| 105 |
all_documents.extend(documents)
|
| 106 |
+
except Exception as e: # pragma: no cover
|
| 107 |
logger.info(
|
| 108 |
f" Error loading pattern {pattern}: {e}"
|
| 109 |
f" - skipping this pattern"
|
|
|
|
| 113 |
logger.info(f"Loaded {len(all_documents)} total local documents.")
|
| 114 |
return all_documents
|
| 115 |
|
| 116 |
+
def _load_github_documents(
|
| 117 |
+
self,
|
| 118 |
+
repos: Optional[List[str]] = None,
|
| 119 |
+
file_filter: Optional[Callable[[str], bool]] = None,
|
| 120 |
+
cleanup_tmp: bool = True
|
| 121 |
) -> List[Document]:
|
| 122 |
"""
|
| 123 |
Load documents from GitHub repositories.
|
|
|
|
| 126 |
|
| 127 |
Args:
|
| 128 |
repos: List of repos (owner/repo format). Defaults to github_repos from init.
|
| 129 |
+
file_filter: Optional filter function for files. If None, uses default filter
|
| 130 |
+
excluding README, CONTRIBUTING, CODE_OF_CONDUCT, and SECURITY files.
|
| 131 |
cleanup_tmp: If True, remove tmp directory before loading.
|
| 132 |
|
| 133 |
Returns:
|
| 134 |
List of loaded documents from all repos.
|
| 135 |
"""
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
# Default filter excludes common documentation files that degrade RAG quality
|
| 138 |
if file_filter is None:
|
| 139 |
+
def default_file_filter(fp: str) -> bool:
|
| 140 |
+
"""Default filter excludes contributing docs to preserve RAG quality.
|
| 141 |
|
| 142 |
+
Implements FR-002 (Knowledge Retrieval): Filters out common boilerplate
|
| 143 |
+
files (README, CONTRIBUTING, etc.) that aren't representative of
|
| 144 |
+
personified agent knowledge.
|
| 145 |
"""
|
|
|
|
| 146 |
basename = os.path.basename(fp).lower()
|
| 147 |
+
# Exclude common boilerplate that doesn't represent agent's knowledge
|
| 148 |
+
excluded = {"readme.md", "contributing.md", "code_of_conduct.md",
|
| 149 |
+
"security.md"}
|
| 150 |
+
return basename not in excluded
|
| 151 |
+
file_filter = default_file_filter
|
| 152 |
|
| 153 |
all_docs = []
|
| 154 |
# Clean up tmp directory before loading
|
|
|
|
| 158 |
logger.info(f"Cleaning up existing tmp directory: {tmp_dir}")
|
| 159 |
shutil.rmtree(tmp_dir)
|
| 160 |
|
| 161 |
+
# Use provided repos or default to empty list if none specified
|
| 162 |
+
repos_to_load = repos if repos is not None else []
|
| 163 |
+
logger.info(f"Loading GitHub documents from {len(repos_to_load)} repos {repos_to_load}")
|
| 164 |
+
for repo in repos_to_load:
|
| 165 |
logger.info(f"Loading GitHub repo: {repo}")
|
| 166 |
try:
|
| 167 |
+
# Clone repo using GitLoader (even though it doesn't load files)
|
| 168 |
+
repo_path = f"{tmp_dir}/{repo}"
|
| 169 |
loader = GitLoader(
|
| 170 |
clone_url=f"https://github.com/{repo}",
|
| 171 |
+
repo_path=repo_path,
|
|
|
|
| 172 |
branch="main",
|
| 173 |
)
|
| 174 |
+
# GitLoader.load() doesn't return files, but it clones the repo
|
| 175 |
+
# so we use DirectoryLoader to actually load the markdown files
|
| 176 |
+
loader.load()
|
| 177 |
+
|
| 178 |
+
# Now use DirectoryLoader to load markdown files from the cloned repo
|
| 179 |
+
directory_loader = DirectoryLoader(
|
| 180 |
+
repo_path,
|
| 181 |
+
glob="**/*.md",
|
| 182 |
+
loader_cls=TextLoader,
|
| 183 |
+
loader_kwargs={'encoding': 'utf-8'}
|
| 184 |
+
)
|
| 185 |
+
docs = directory_loader.load()
|
| 186 |
+
|
| 187 |
+
# Apply filter (default or custom) to exclude irrelevant files
|
| 188 |
+
docs = [doc for doc in docs if file_filter(doc.metadata['source'])]
|
| 189 |
|
| 190 |
# Add repo metadata to each document
|
| 191 |
for doc in docs:
|
| 192 |
doc.metadata["github_repo"] = repo
|
| 193 |
|
| 194 |
logger.info(f" Loaded {len(docs)} documents from {repo}")
|
| 195 |
+
all_docs.extend(docs)
|
| 196 |
+
except Exception as e: # pragma: no cover
|
| 197 |
logger.info(f" Error loading repo {repo}: {e} - skipping")
|
| 198 |
continue
|
| 199 |
|
|
|
|
| 252 |
strip_headers=False)
|
| 253 |
|
| 254 |
all_chunks = []
|
| 255 |
+
chunk_index = 0 # Track chunk number across all documents
|
| 256 |
for doc in documents:
|
| 257 |
# Split by headers first - this returns Documents with header metadata
|
| 258 |
header_chunks = header_splitter.split_text(doc.page_content)
|
|
|
|
| 261 |
for chunk in header_chunks:
|
| 262 |
# Create new Document with combined metadata
|
| 263 |
merged_metadata = {**doc.metadata, **chunk.metadata}
|
| 264 |
+
merged_metadata['chunk_index'] = chunk_index # Add global chunk index
|
| 265 |
+
chunk_index += 1
|
| 266 |
new_doc = Document(
|
| 267 |
page_content=chunk.page_content,
|
| 268 |
metadata=merged_metadata,
|
|
|
|
| 273 |
size_splitter = MarkdownTextSplitter(chunk_size=self.config.chunk_size)
|
| 274 |
final_chunks = size_splitter.split_documents(all_chunks)
|
| 275 |
|
| 276 |
+
# Re-index after size splitting to maintain sequential chunk indices
|
| 277 |
+
for i, chunk in enumerate(final_chunks):
|
| 278 |
+
chunk.metadata['chunk_index'] = i
|
| 279 |
+
|
| 280 |
logger.info(f"Created {len(final_chunks)} chunks")
|
| 281 |
return final_chunks
|
| 282 |
|
| 283 |
+
def load_and_process_all(self, github_repos: Optional[List[str]] = None) -> List[Document]:
|
| 284 |
"""
|
| 285 |
Load, process, and chunk all documents. Automatically loads local documents
|
| 286 |
if doc_load_local is set, and GitHub documents if github_repos (or
|
|
|
|
| 301 |
if self.config.doc_load_local:
|
| 302 |
all_docs.extend(self.load_local_documents())
|
| 303 |
|
| 304 |
+
# Load GitHub documents if repos are provided (github_repos must come from caller)
|
| 305 |
+
if github_repos:
|
| 306 |
+
all_docs.extend(self._load_github_documents(repos=github_repos))
|
|
|
|
| 307 |
|
| 308 |
processed_docs = self.process_documents(all_docs)
|
| 309 |
chunks = self.chunk_documents(processed_docs)
|
|
|
|
| 343 |
chroma_client = chromadb.EphemeralClient(Settings(anonymized_telemetry=False))
|
| 344 |
|
| 345 |
# Drop existing collection if requested
|
| 346 |
+
if reset: # pragma: no cover
|
| 347 |
try:
|
| 348 |
chroma_client.delete_collection(self.config.db_name)
|
| 349 |
logger.info(f"Dropped existing collection: {self.config.db_name}")
|
| 350 |
+
except Exception: # pragma: no cover
|
| 351 |
pass # Collection doesn't exist yet
|
| 352 |
|
| 353 |
logger.info(f"Creating vectorstore with {len(chunks)} chunks...")
|
|
|
|
| 361 |
count = vectorstore._collection.count()
|
| 362 |
logger.info(f"Vectorstore created with {count} documents")
|
| 363 |
|
| 364 |
+
self.vectorstore = vectorstore
|
| 365 |
return vectorstore
|
| 366 |
|
| 367 |
def setup_vectorstore(
|
| 368 |
+
self, github_repos: Optional[List[str]] = None, reset: bool = True
|
| 369 |
) -> Chroma:
|
| 370 |
"""
|
| 371 |
Complete pipeline: load, process, chunk, and create vectorstore. Automatically
|
|
|
|
| 386 |
chunks = self.load_and_process_all(github_repos=github_repos)
|
| 387 |
return self.create_vectorstore(chunks, reset=reset)
|
| 388 |
|
| 389 |
+
def show_docs_for_file(self, filename: str): # pragma: no cover
|
| 390 |
"""
|
| 391 |
Retrieve and print chunks from the vectorstore whose metadata['file_path'] ends with the
|
| 392 |
given filename. Returns a list of (doc_id, metadata, document).
|
| 393 |
|
| 394 |
DEBUG TOOL: Utility/debugging function - no corresponding FR/NFR.
|
| 395 |
"""
|
| 396 |
+
if self.vectorstore is None:
|
| 397 |
+
logger.warning(
|
| 398 |
+
"Vectorstore not initialized. Call setup_vectorstore() first."
|
| 399 |
+
)
|
| 400 |
+
return []
|
| 401 |
+
|
| 402 |
+
all_docs = self.vectorstore.get()
|
| 403 |
logger.info(f"Searching for chunks from file: {filename}")
|
| 404 |
|
| 405 |
ids = all_docs.get("ids", [])
|
|
|
|
| 420 |
logger.info("=" * 100)
|
| 421 |
logger.info(content)
|
| 422 |
logger.info("")
|
| 423 |
+
|
| 424 |
+
return matched
|
|
|
|
|
|
|
|
|
src/notebooks/experiments.ipynb
CHANGED
|
@@ -16,10 +16,7 @@
|
|
| 16 |
"outputs": [],
|
| 17 |
"source": [
|
| 18 |
"# Setup configuration\n",
|
| 19 |
-
"import
|
| 20 |
-
"sys.path.append('/Users/benyoung/projects/ai-me')\n",
|
| 21 |
-
"\n",
|
| 22 |
-
"from src.config import Config\n",
|
| 23 |
"from IPython.display import Markdown\n",
|
| 24 |
"from agents import trace, Runner\n",
|
| 25 |
"\n",
|
|
@@ -44,9 +41,9 @@
|
|
| 44 |
"outputs": [],
|
| 45 |
"source": [
|
| 46 |
"from importlib import reload\n",
|
| 47 |
-
"import
|
| 48 |
"reload(_data_module)\n",
|
| 49 |
-
"from
|
| 50 |
"\n",
|
| 51 |
"\n",
|
| 52 |
"# Use consolidated data manager\n",
|
|
@@ -85,7 +82,7 @@
|
|
| 85 |
"metadata": {},
|
| 86 |
"outputs": [],
|
| 87 |
"source": [
|
| 88 |
-
"from
|
| 89 |
"\n",
|
| 90 |
"# Initialize agent config with vectorstore\n",
|
| 91 |
"agent_config = AIMeAgent(\n",
|
|
@@ -95,7 +92,7 @@
|
|
| 95 |
" github_token=config.github_token\n",
|
| 96 |
")\n",
|
| 97 |
"\n",
|
| 98 |
-
"ai_me = await agent_config.create_ai_me_agent()
|
| 99 |
]
|
| 100 |
},
|
| 101 |
{
|
|
@@ -275,9 +272,9 @@
|
|
| 275 |
"outputs": [],
|
| 276 |
"source": [
|
| 277 |
"# Reload agent module to pick up latest changes\n",
|
| 278 |
-
"import
|
| 279 |
"reload(_agent_module)\n",
|
| 280 |
-
"from
|
| 281 |
"\n",
|
| 282 |
"# Recreate agent config with updated module\n",
|
| 283 |
"agent_config = AIMeAgent(\n",
|
|
|
|
| 16 |
"outputs": [],
|
| 17 |
"source": [
|
| 18 |
"# Setup configuration\n",
|
| 19 |
+
"from config import Config\n",
|
|
|
|
|
|
|
|
|
|
| 20 |
"from IPython.display import Markdown\n",
|
| 21 |
"from agents import trace, Runner\n",
|
| 22 |
"\n",
|
|
|
|
| 41 |
"outputs": [],
|
| 42 |
"source": [
|
| 43 |
"from importlib import reload\n",
|
| 44 |
+
"import data as _data_module\n",
|
| 45 |
"reload(_data_module)\n",
|
| 46 |
+
"from data import DataManager, DataManagerConfig\n",
|
| 47 |
"\n",
|
| 48 |
"\n",
|
| 49 |
"# Use consolidated data manager\n",
|
|
|
|
| 82 |
"metadata": {},
|
| 83 |
"outputs": [],
|
| 84 |
"source": [
|
| 85 |
+
"from agent import AIMeAgent\n",
|
| 86 |
"\n",
|
| 87 |
"# Initialize agent config with vectorstore\n",
|
| 88 |
"agent_config = AIMeAgent(\n",
|
|
|
|
| 92 |
" github_token=config.github_token\n",
|
| 93 |
")\n",
|
| 94 |
"\n",
|
| 95 |
+
"ai_me = await agent_config.create_ai_me_agent()"
|
| 96 |
]
|
| 97 |
},
|
| 98 |
{
|
|
|
|
| 272 |
"outputs": [],
|
| 273 |
"source": [
|
| 274 |
"# Reload agent module to pick up latest changes\n",
|
| 275 |
+
"import agent as _agent_module\n",
|
| 276 |
"reload(_agent_module)\n",
|
| 277 |
+
"from agent import AIMeAgent\n",
|
| 278 |
"\n",
|
| 279 |
"# Recreate agent config with updated module\n",
|
| 280 |
"agent_config = AIMeAgent(\n",
|
tests/data/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test Data Directory
|
| 2 |
+
|
| 3 |
+
This directory contains controlled test data for RAG (Retrieval Augmented Generation) testing in the ai-me project.
|
| 4 |
+
|
| 5 |
+
## Purpose
|
| 6 |
+
|
| 7 |
+
These markdown files provide known content for deterministic testing of:
|
| 8 |
+
1. Document loading and chunking (from local files)
|
| 9 |
+
2. Vector embeddings and storage (ChromaDB)
|
| 10 |
+
3. Retrieval quality (similarity search)
|
| 11 |
+
4. Agent response accuracy (RAG output validation)
|
| 12 |
+
|
| 13 |
+
## Files
|
| 14 |
+
|
| 15 |
+
| File | Purpose | Key Content | Used By Tests |
|
| 16 |
+
|------|---------|-------------|---------------|
|
| 17 |
+
| **rear_info.md** | ReaR disaster recovery info | Project ID: IT-245 | test_rear_knowledge_contains_it245 |
|
| 18 |
+
| **projects.md** | Project listings | IT-245, IT-300, APP-101, DATA-500 | General project queries |
|
| 19 |
+
| **team_info.md** | Team structure (fictional) | Alice, Bob, Carol + departments | Person/team queries |
|
| 20 |
+
| **faq.md** | FAQ with tech stack, workflows | IT-245 references, dev processes | General knowledge queries |
|
| 21 |
+
| **README.md** | This documentation | Test data guide | - |
|
| 22 |
+
|
| 23 |
+
## Statistics
|
| 24 |
+
|
| 25 |
+
- **Total Files**: 5 markdown files
|
| 26 |
+
- **Total Chunks**: ~38 (after splitting with CharacterTextSplitter)
|
| 27 |
+
- **Chunk Size**: 2500 characters (default)
|
| 28 |
+
- **Chunk Overlap**: 0 characters (default)
|
| 29 |
+
- **Embedding Model**: sentence-transformers/all-MiniLM-L6-v2
|
| 30 |
+
|
| 31 |
+
## Usage in Tests
|
| 32 |
+
|
| 33 |
+
The test suite (`tests/integration/spec-001.py`) automatically uses this directory:
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
# Configuration in tests/integration/spec-001.py
|
| 37 |
+
os.environ["GITHUB_REPOS"] = "" # Disable GitHub loading
|
| 38 |
+
test_data_dir = os.path.join(project_root, "tests", "data")
|
| 39 |
+
|
| 40 |
+
# DataManager initialization with required config
|
| 41 |
+
config = DataManagerConfig(
|
| 42 |
+
doc_load_local=["**/*.md"],
|
| 43 |
+
github_repos=[],
|
| 44 |
+
doc_root=test_data_dir # Points to this directory
|
| 45 |
+
)
|
| 46 |
+
data_manager = DataManager(config=config)
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Test Cases
|
| 50 |
+
|
| 51 |
+
### ✅ Test 1: ReaR Knowledge (IT-245)
|
| 52 |
+
**Query**: "What do you know about ReaR?"
|
| 53 |
+
|
| 54 |
+
**Source**: `rear_info.md`
|
| 55 |
+
|
| 56 |
+
**Validates**:
|
| 57 |
+
- Document retrieval works correctly
|
| 58 |
+
- Agent finds and extracts specific project information
|
| 59 |
+
- Response contains "IT-245" identifier
|
| 60 |
+
|
| 61 |
+
**Expected Output**: Response mentions ReaR, disaster recovery, and IT-245 project.
|
| 62 |
+
|
| 63 |
+
### ⏭️ Test 2: GitHub Commits (Skipped)
|
| 64 |
+
**Note**: Requires MCP servers (disabled for test speed).
|
| 65 |
+
|
| 66 |
+
### ✅ Test 3: Unknown Person (Negative Test)
|
| 67 |
+
**Query**: "Who is slartibartfast?"
|
| 68 |
+
|
| 69 |
+
**Source**: None (intentionally missing)
|
| 70 |
+
|
| 71 |
+
**Validates**:
|
| 72 |
+
- Agent handles missing information gracefully
|
| 73 |
+
- No hallucination or fabricated responses
|
| 74 |
+
- Proper "don't have information" response
|
| 75 |
+
|
| 76 |
+
**Expected Output**: Response contains negative indicators like "don't have", "no information", etc.
|
| 77 |
+
|
| 78 |
+
## Benefits vs. Loading from GitHub
|
| 79 |
+
|
| 80 |
+
| Aspect | Test Data Directory | GitHub Loading |
|
| 81 |
+
|--------|-------------------|----------------|
|
| 82 |
+
| **Speed** | ~10 seconds total | Minutes per test run |
|
| 83 |
+
| **Network** | None required | API calls needed |
|
| 84 |
+
| **Determinism** | Fully controlled | May change over time |
|
| 85 |
+
| **Setup** | Already included | Requires GitHub token |
|
| 86 |
+
| **Isolation** | Completely isolated | External dependency |
|
| 87 |
+
|
| 88 |
+
## Key Implementation Details
|
| 89 |
+
|
| 90 |
+
### Local Document Metadata
|
| 91 |
+
|
| 92 |
+
Unlike GitHub documents, local documents have simplified metadata:
|
| 93 |
+
|
| 94 |
+
```python
|
| 95 |
+
# GitHub documents have:
|
| 96 |
+
doc.metadata['github_repo'] = 'owner/repo'
|
| 97 |
+
doc.metadata['file_path'] = 'path/to/file.md'
|
| 98 |
+
|
| 99 |
+
# Local documents have:
|
| 100 |
+
doc.metadata['source'] = '/full/path/to/file.md'
|
| 101 |
+
# NO github_repo field
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The `get_local_info` tool in `src/agent.py` was updated to handle both cases.
|
| 105 |
+
|
| 106 |
+
### Unicode Handling
|
| 107 |
+
|
| 108 |
+
Test assertions handle Unicode variants:
|
| 109 |
+
|
| 110 |
+
- **Hyphens**: `IT-245` (regular) vs `IT‑245` (non-breaking)
|
| 111 |
+
- **Apostrophes**: `don't` (regular) vs `don't` (smart quote)
|
| 112 |
+
- **Spaces**: Regular space vs non-breaking space (`\u00a0`)
|
| 113 |
+
|
| 114 |
+
## Adding New Test Data
|
| 115 |
+
|
| 116 |
+
To add new test content:
|
| 117 |
+
|
| 118 |
+
1. **Create markdown file** in this directory:
|
| 119 |
+
```bash
|
| 120 |
+
touch tests/data/my_topic.md
|
| 121 |
+
# Add relevant content with known facts
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
2. **Add test case** in `tests/integration/spec-001.py`:
|
| 125 |
+
```python
|
| 126 |
+
@pytest.mark.asyncio
|
| 127 |
+
async def test_my_topic_knowledge(ai_me_agent):
|
| 128 |
+
query = "What do you know about [topic]?"
|
| 129 |
+
result = await Runner.run(ai_me_agent, query, max_turns=30)
|
| 130 |
+
assert "[expected_content]" in result.final_output
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
3. **Document** in this README
|
| 134 |
+
|
| 135 |
+
4. **Verify** chunks created:
|
| 136 |
+
```bash
|
| 137 |
+
uv run pytest tests/ -v -s | grep "Created.*chunks"
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## Maintenance Guidelines
|
| 141 |
+
|
| 142 |
+
### What TO Include
|
| 143 |
+
|
| 144 |
+
✅ Fictional but realistic data
|
| 145 |
+
✅ Specific identifiers for testing (e.g., IT-245)
|
| 146 |
+
✅ Structured markdown with clear headings
|
| 147 |
+
✅ Cross-references between documents
|
| 148 |
+
✅ Both positive and negative test cases
|
| 149 |
+
|
| 150 |
+
### What NOT to Include
|
| 151 |
+
|
| 152 |
+
❌ Real personal information or PII
|
| 153 |
+
❌ Sensitive company data
|
| 154 |
+
❌ Large binary files or images
|
| 155 |
+
❌ External dependencies
|
| 156 |
+
❌ Dynamic/time-sensitive content
|
| 157 |
+
|
| 158 |
+
## Troubleshooting
|
| 159 |
+
|
| 160 |
+
### "Vectorstore setup complete with 0 documents"
|
| 161 |
+
**Cause**: Files not loading from tests/data directory
|
| 162 |
+
|
| 163 |
+
**Fix**: Verify `doc_root` parameter and file patterns
|
| 164 |
+
|
| 165 |
+
### "Expected 'IT-245' in response but got..."
|
| 166 |
+
**Cause**: LLM used Unicode non-breaking hyphen
|
| 167 |
+
|
| 168 |
+
**Fix**: Test already handles both variants, check for other formatting
|
| 169 |
+
|
| 170 |
+
### Test execution is slow (> 30 seconds)
|
| 171 |
+
**Cause**: May be loading from GitHub instead of tests/data
|
| 172 |
+
|
| 173 |
+
**Fix**: Verify `GITHUB_REPOS=""` in test environment setup
|
| 174 |
+
|
| 175 |
+
## Performance Benchmarks
|
| 176 |
+
|
| 177 |
+
Measured on M1 MacBook Pro:
|
| 178 |
+
|
| 179 |
+
- **Vectorstore Setup**: 2-3 seconds (includes embedding model loading)
|
| 180 |
+
- **Test 1 (ReaR)**: 3-4 seconds (includes LLM calls)
|
| 181 |
+
- **Test 3 (Unknown)**: 3-4 seconds
|
| 182 |
+
- **Total Runtime**: ~10 seconds for all passing tests
|
| 183 |
+
|
| 184 |
+
Compare to production setup with GitHub repos: 2-5 minutes
|
| 185 |
+
|
| 186 |
+
## Future Enhancements
|
| 187 |
+
|
| 188 |
+
- [ ] Add more domain-specific test documents
|
| 189 |
+
- [ ] Create test cases for multi-document synthesis
|
| 190 |
+
- [ ] Add edge cases (empty files, malformed markdown)
|
| 191 |
+
- [ ] Performance regression tests
|
| 192 |
+
- [ ] Quality metrics (retrieval precision/recall)
|
| 193 |
+
|
| 194 |
+
For more details, see `/TESTING.md` in the project root.
|
tests/data/projects.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Active Projects
|
| 2 |
+
|
| 3 |
+
## Infrastructure Projects
|
| 4 |
+
|
| 5 |
+
### IT-245: Disaster Recovery
|
| 6 |
+
**Status**: In Progress
|
| 7 |
+
**Owner**: Infrastructure Team
|
| 8 |
+
**Timeline**: Q4 2024 - Q1 2025
|
| 9 |
+
|
| 10 |
+
Implementation of Relax-and-Recover (ReaR) backup solution across production servers. This provides automated disaster recovery capabilities with bare metal restoration.
|
| 11 |
+
|
| 12 |
+
**Key Milestones**:
|
| 13 |
+
- ✅ Proof of concept completed
|
| 14 |
+
- ✅ Test environment deployment
|
| 15 |
+
- 🔄 Production rollout (in progress)
|
| 16 |
+
- ⏳ Documentation and training
|
| 17 |
+
|
| 18 |
+
### IT-300: Network Segmentation
|
| 19 |
+
**Status**: Planning
|
| 20 |
+
**Owner**: Security Team
|
| 21 |
+
**Timeline**: Q1 2025
|
| 22 |
+
|
| 23 |
+
Network redesign to implement zero-trust architecture with micro-segmentation.
|
| 24 |
+
|
| 25 |
+
## Application Projects
|
| 26 |
+
|
| 27 |
+
### APP-101: Customer Portal v2
|
| 28 |
+
**Status**: Active Development
|
| 29 |
+
**Owner**: Frontend Team
|
| 30 |
+
**Timeline**: Q4 2024 - Q2 2025
|
| 31 |
+
|
| 32 |
+
Complete redesign of customer-facing portal with modern React architecture.
|
| 33 |
+
|
| 34 |
+
### APP-150: API Gateway Upgrade
|
| 35 |
+
**Status**: In Progress
|
| 36 |
+
**Owner**: Backend Team
|
| 37 |
+
**Timeline**: Q4 2024
|
| 38 |
+
|
| 39 |
+
Migrating from legacy API gateway to Kong with enhanced security and monitoring.
|
| 40 |
+
|
| 41 |
+
## Data Projects
|
| 42 |
+
|
| 43 |
+
### DATA-500: Analytics Platform
|
| 44 |
+
**Status**: Active Development
|
| 45 |
+
**Owner**: Data Engineering Team
|
| 46 |
+
**Timeline**: Q3 2024 - Q2 2025
|
| 47 |
+
|
| 48 |
+
Building modern data warehouse on Snowflake with real-time analytics capabilities.
|
| 49 |
+
|
| 50 |
+
### DATA-510: ML Pipeline
|
| 51 |
+
**Status**: Planning
|
| 52 |
+
**Owner**: Data Science Team
|
| 53 |
+
**Timeline**: Q1 2025 - Q3 2025
|
| 54 |
+
|
| 55 |
+
Automated machine learning pipeline for predictive analytics and recommendation systems.
|
| 56 |
+
|
| 57 |
+
## Completed Projects
|
| 58 |
+
|
| 59 |
+
- **IT-200**: VMware to KVM migration (Completed Q3 2024)
|
| 60 |
+
- **APP-90**: Mobile app release (Completed Q2 2024)
|
| 61 |
+
- **SEC-400**: SOC 2 compliance (Completed Q1 2024)
|
tests/data/team.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Team Information
|
| 2 |
+
|
| 3 |
+
## Operation Agenetic Me
|
| 4 |
+
|
| 5 |
+
### Engineers
|
| 6 |
+
|
| 7 |
+
- **Ben Young** - Lead Software Engineer
|
| 8 |
+
- Specializes in backend systems
|
| 9 |
+
- Working on microservices architecture
|
| 10 |
+
- Programming languages: Python, Go, TypeScript, Rust, SQL
|
| 11 |
+
- Expertise in: async programming, distributed systems, cloud infrastructure
|
| 12 |
+
- Contact: ben@example.com
|
| 13 |
+
|
| 14 |
+
- **Bob Smith** - Frontend Developer
|
| 15 |
+
- React and TypeScript expert
|
| 16 |
+
- UI/UX design background
|
| 17 |
+
- Contact: bob@example.com
|
| 18 |
+
|
| 19 |
+
### Product Managers
|
| 20 |
+
|
| 21 |
+
- **Carol Williams** - Product Owner
|
| 22 |
+
- Write requirements
|
| 23 |
+
- Educate team on user profiles
|
| 24 |
+
- Contact: carol@example.com
|
| 25 |
+
|
| 26 |
+
### Recent Projects
|
| 27 |
+
|
| 28 |
+
1. **Project Phoenix** - Cloud migration initiative
|
| 29 |
+
2. **Project Titan** - New customer portal
|
| 30 |
+
3. **IT-245** - ReaR disaster recovery implementation
|
| 31 |
+
|
| 32 |
+
## Department Structure
|
| 33 |
+
|
| 34 |
+
- Engineering Director: David Chen
|
| 35 |
+
- Product Manager: Emma Davis
|
| 36 |
+
- QA Lead: Frank Miller
|
| 37 |
+
|
| 38 |
+
## Office Locations
|
| 39 |
+
|
| 40 |
+
- San Francisco HQ
|
| 41 |
+
- Austin Remote Office
|
| 42 |
+
- London European Hub
|
| 43 |
+
|
| 44 |
+
## Team Events
|
| 45 |
+
|
| 46 |
+
- Weekly stand-ups: Monday 9 AM PST
|
| 47 |
+
- Sprint planning: Every other Wednesday
|
| 48 |
+
- Team retrospectives: Last Friday of the month
|
| 49 |
+
- Quarterly all-hands: First week of Q1, Q2, Q3, Q4
|
tests/integration/spec-001.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Integration tests for ai-me agent.
|
| 3 |
+
Tests the complete setup including vectorstore, agent configuration, and agent responses.
|
| 4 |
+
"""
|
| 5 |
+
import pytest
|
| 6 |
+
import pytest_asyncio
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import logging
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from unittest.mock import AsyncMock, patch
|
| 13 |
+
|
| 14 |
+
# Something about these tests makes me feel yucky. Big, brittle, and slow. BBS?
|
| 15 |
+
# In the future we should run inference locally with docker-compose models.
|
| 16 |
+
|
| 17 |
+
# Set temperature and seed for deterministic test results
|
| 18 |
+
os.environ["TEMPERATURE"] = "0"
|
| 19 |
+
os.environ["SEED"] = "42"
|
| 20 |
+
|
| 21 |
+
# Point our RAG to the tests/data directory
|
| 22 |
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
| 23 |
+
test_data_dir = os.path.join(project_root, "tests", "data")
|
| 24 |
+
os.environ["DOC_ROOT"] = test_data_dir
|
| 25 |
+
os.environ["LOCAL_DOCS"] = "**/*.md"
|
| 26 |
+
|
| 27 |
+
from config import setup_logger, Config
|
| 28 |
+
from agent import AIMeAgent
|
| 29 |
+
from data import DataManager, DataManagerConfig
|
| 30 |
+
|
| 31 |
+
logger = setup_logger(__name__)
|
| 32 |
+
|
| 33 |
+
# ============================================================================
|
| 34 |
+
# SHARED CACHING - Initialize on first use, then reuse
|
| 35 |
+
# ============================================================================
|
| 36 |
+
|
| 37 |
+
_config = None
|
| 38 |
+
_vectorstore = None
|
| 39 |
+
_data_manager = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _get_shared_config():
|
| 43 |
+
"""Lazy initialization of shared config."""
|
| 44 |
+
global _config
|
| 45 |
+
if _config is None:
|
| 46 |
+
_config = Config() # type: ignore
|
| 47 |
+
logger.info(f"Initialized shared config: {_config.bot_full_name}")
|
| 48 |
+
return _config
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _get_shared_vectorstore():
|
| 52 |
+
"""Lazy initialization of shared vectorstore."""
|
| 53 |
+
global _vectorstore, _data_manager
|
| 54 |
+
if _vectorstore is None:
|
| 55 |
+
logger.info("Initializing shared vectorstore (first test)...")
|
| 56 |
+
test_data_dir = os.path.join(project_root, "tests", "data")
|
| 57 |
+
_data_config = DataManagerConfig(
|
| 58 |
+
doc_root=test_data_dir
|
| 59 |
+
)
|
| 60 |
+
_data_manager = DataManager(config=_data_config)
|
| 61 |
+
_vectorstore = _data_manager.setup_vectorstore()
|
| 62 |
+
logger.info(f"Shared vectorstore ready: {_vectorstore._collection.count()} documents")
|
| 63 |
+
return _vectorstore
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@pytest_asyncio.fixture(scope="function")
|
| 67 |
+
async def ai_me_agent():
|
| 68 |
+
"""
|
| 69 |
+
Setup fixture for ai-me agent with vectorstore and MCP servers.
|
| 70 |
+
|
| 71 |
+
CRITICAL: Function-scoped fixture prevents hanging/blocking issues.
|
| 72 |
+
Each test gets its own agent instance with proper cleanup.
|
| 73 |
+
|
| 74 |
+
Reuses shared config and vectorstore (lazy-initialized on first use).
|
| 75 |
+
|
| 76 |
+
This fixture:
|
| 77 |
+
- Reuses shared config and vectorstore
|
| 78 |
+
- Creates agent WITH real subprocess MCP servers (GitHub, Time, Memory)
|
| 79 |
+
- Yields agent for test
|
| 80 |
+
- Cleans up MCP servers after test completes
|
| 81 |
+
"""
|
| 82 |
+
config = _get_shared_config()
|
| 83 |
+
vectorstore = _get_shared_vectorstore()
|
| 84 |
+
|
| 85 |
+
# Initialize agent config with shared vectorstore
|
| 86 |
+
aime_agent = AIMeAgent(
|
| 87 |
+
bot_full_name=config.bot_full_name,
|
| 88 |
+
model=config.model,
|
| 89 |
+
vectorstore=vectorstore,
|
| 90 |
+
github_token=config.github_token,
|
| 91 |
+
session_id="test-session"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Create the agent WITH MCP servers enabled
|
| 95 |
+
logger.info("Creating ai-me agent with MCP servers...")
|
| 96 |
+
assert aime_agent.session_id is not None, "session_id should be set"
|
| 97 |
+
await aime_agent.create_ai_me_agent(
|
| 98 |
+
mcp_params=[
|
| 99 |
+
aime_agent.mcp_github_params,
|
| 100 |
+
aime_agent.mcp_time_params,
|
| 101 |
+
aime_agent.get_mcp_memory_params(aime_agent.session_id),
|
| 102 |
+
]
|
| 103 |
+
)
|
| 104 |
+
logger.info("Agent created successfully with MCP servers")
|
| 105 |
+
logger.info(f"Temperature set to {config.temperature}")
|
| 106 |
+
logger.info(f"Seed set to {config.seed}")
|
| 107 |
+
|
| 108 |
+
# Yield the agent for the test
|
| 109 |
+
yield aime_agent
|
| 110 |
+
|
| 111 |
+
# CRITICAL: Cleanup after test completes to prevent hanging
|
| 112 |
+
logger.info("Cleaning up MCP servers after test...")
|
| 113 |
+
await aime_agent.cleanup()
|
| 114 |
+
logger.info("Cleanup complete")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@pytest.mark.asyncio
|
| 118 |
+
async def test_github_documents_load():
|
| 119 |
+
"""Tests FR-002: GitHub document loading with source metadata."""
|
| 120 |
+
config = Config() # type: ignore
|
| 121 |
+
|
| 122 |
+
# Load GitHub documents directly
|
| 123 |
+
github_config = DataManagerConfig(
|
| 124 |
+
doc_load_local=[]
|
| 125 |
+
)
|
| 126 |
+
dm = DataManager(config=github_config)
|
| 127 |
+
vs = dm.setup_vectorstore(github_repos=["byoung/ai-me"])
|
| 128 |
+
|
| 129 |
+
agent = AIMeAgent(
|
| 130 |
+
bot_full_name=config.bot_full_name,
|
| 131 |
+
model=config.model,
|
| 132 |
+
vectorstore=vs,
|
| 133 |
+
github_token=config.github_token,
|
| 134 |
+
session_id="test-session"
|
| 135 |
+
)
|
| 136 |
+
await agent.create_ai_me_agent()
|
| 137 |
+
|
| 138 |
+
response = await agent.run("Do you have python experience?")
|
| 139 |
+
|
| 140 |
+
assert "yes" in response.lower(), (
|
| 141 |
+
f"yes' in response but got: {response}"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@pytest.mark.asyncio
|
| 146 |
+
async def test_rear_knowledge_contains_it245(ai_me_agent):
|
| 147 |
+
"""Tests REQ-001: Knowledge base retrieval of personal documentation."""
|
| 148 |
+
response = await ai_me_agent.run("What is IT-245?")
|
| 149 |
+
|
| 150 |
+
assert "IT-245" in response or "It-245" in response or "it-245" in response
|
| 151 |
+
logger.info("✓ IT-245 found in response")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@pytest.mark.asyncio
|
| 155 |
+
async def test_github_commits_contains_shas(ai_me_agent):
|
| 156 |
+
"""Tests REQ-002: MCP GitHub integration - retrieve commit history."""
|
| 157 |
+
response = await ai_me_agent.run("What are some recent commits I've made?")
|
| 158 |
+
|
| 159 |
+
assert response, "Response is empty"
|
| 160 |
+
assert len(response) > 10, "Response is too short"
|
| 161 |
+
logger.info("✓ Response contains commit information")
|
| 162 |
+
|
| 163 |
+
@pytest.mark.asyncio
|
| 164 |
+
async def test_unknown_person_contains_negative_response(ai_me_agent):
|
| 165 |
+
"""Tests REQ-003: Graceful handling of out-of-scope requests."""
|
| 166 |
+
response = await ai_me_agent.run(
|
| 167 |
+
"Do you know Slartibartfast?" # Presumed unknown person
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
assert response, "Response is empty"
|
| 171 |
+
assert (
|
| 172 |
+
"don't know" in response.lower()
|
| 173 |
+
or "not familiar" in response.lower()
|
| 174 |
+
or "no information" in response.lower()
|
| 175 |
+
or "don't have any information" in response.lower()
|
| 176 |
+
), f"Response doesn't indicate lack of knowledge: {response}"
|
| 177 |
+
logger.info(f"✓ Test passed - correctly handled out-of-scope query")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@pytest.mark.asyncio
|
| 181 |
+
async def test_carol_knowledge_contains_product(ai_me_agent):
|
| 182 |
+
"""Tests FR-002, FR-003: Verify asking about Carol returns 'product'."""
|
| 183 |
+
response_raw = await ai_me_agent.run("Do you know Carol?")
|
| 184 |
+
response = response_raw.lower() # Convert to lowercase for matching
|
| 185 |
+
|
| 186 |
+
# Assert that 'product' appears in the response (Carol is Product Owner)
|
| 187 |
+
assert "product" in response, (
|
| 188 |
+
f"Expected 'product' in response but got: {response}"
|
| 189 |
+
)
|
| 190 |
+
logger.info("✓ Test passed: Response contains 'product'")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@pytest.mark.asyncio
|
| 194 |
+
async def test_mcp_time_server_returns_current_date(ai_me_agent):
|
| 195 |
+
"""Tests FR-009, NFR-001: Verify that the MCP time server returns the current date."""
|
| 196 |
+
response = await ai_me_agent.run("What is today's date?")
|
| 197 |
+
|
| 198 |
+
# Check for current date in various formats (ISO or natural language)
|
| 199 |
+
now = datetime.now()
|
| 200 |
+
expected_date, current_year, current_month, current_day = (
|
| 201 |
+
now.strftime("%Y-%m-%d"),
|
| 202 |
+
str(now.year),
|
| 203 |
+
now.strftime("%B"),
|
| 204 |
+
str(now.day),
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Accept either ISO format or natural language date
|
| 208 |
+
has_date = (
|
| 209 |
+
expected_date in response
|
| 210 |
+
or (
|
| 211 |
+
current_year in response
|
| 212 |
+
and current_month in response
|
| 213 |
+
and current_day in response
|
| 214 |
+
)
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
assert has_date, (
|
| 218 |
+
f"Expected response to contain current date "
|
| 219 |
+
f"({expected_date} or {current_month} {current_day}, {current_year}) "
|
| 220 |
+
f"but got: {response}"
|
| 221 |
+
)
|
| 222 |
+
logger.info(f"✓ Test passed: Response contains current date")
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@pytest.mark.asyncio
|
| 226 |
+
async def test_mcp_memory_server_remembers_favorite_color(ai_me_agent):
|
| 227 |
+
"""Tests FR-013, NFR-002:
|
| 228 |
+
Verify that the MCP memory server persists information across interactions.
|
| 229 |
+
"""
|
| 230 |
+
await ai_me_agent.run("My favorite color is chartreuse.")
|
| 231 |
+
response2 = await ai_me_agent.run("What's my favorite color?")
|
| 232 |
+
|
| 233 |
+
# Check that the agent remembers the color
|
| 234 |
+
assert "chartreuse" in response2.lower(), (
|
| 235 |
+
f"Expected agent to remember favorite color 'chartreuse' "
|
| 236 |
+
f"but got: {response2}"
|
| 237 |
+
)
|
| 238 |
+
msg = (
|
| 239 |
+
"✓ Test passed: Agent remembered favorite color 'chartreuse' "
|
| 240 |
+
"across interactions"
|
| 241 |
+
)
|
| 242 |
+
logger.info(msg)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@pytest.mark.asyncio
|
| 246 |
+
async def test_github_relative_links_converted_to_absolute_urls():
|
| 247 |
+
"""Tests FR-004: Document processing converts relative GitHub links to absolute URLs.
|
| 248 |
+
|
| 249 |
+
Validates that when documents are loaded from GitHub with relative links
|
| 250 |
+
(e.g., /resume.md), they are rewritten to full GitHub URLs
|
| 251 |
+
(e.g., https://github.com/owner/repo/blob/main/resume.md).
|
| 252 |
+
|
| 253 |
+
This is a unit-level test of the DataManager.process_documents() method.
|
| 254 |
+
"""
|
| 255 |
+
from langchain_core.documents import Document
|
| 256 |
+
|
| 257 |
+
sample_doc = Document(
|
| 258 |
+
page_content=(
|
| 259 |
+
"Check out [my resume](/resume.md) and "
|
| 260 |
+
"[projects](/projects.md) for more info."
|
| 261 |
+
),
|
| 262 |
+
metadata={
|
| 263 |
+
"source": "github://byoung/ai-me/docs/about.md",
|
| 264 |
+
"github_repo": "byoung/ai-me"
|
| 265 |
+
}
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Verify metadata is set correctly before processing
|
| 269 |
+
assert sample_doc.metadata["github_repo"] == "byoung/ai-me", (
|
| 270 |
+
"Sample doc metadata should have github_repo"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
data_config = DataManagerConfig()
|
| 274 |
+
data_manager = DataManager(config=data_config)
|
| 275 |
+
processed_docs = data_manager.process_documents([sample_doc])
|
| 276 |
+
|
| 277 |
+
assert len(processed_docs) == 1, "Expected 1 processed document"
|
| 278 |
+
processed_content = processed_docs[0].page_content
|
| 279 |
+
|
| 280 |
+
# Check that relative links have been converted to absolute GitHub URLs
|
| 281 |
+
assert "https://github.com/byoung/ai-me/blob/main/resume.md" in processed_content, (
|
| 282 |
+
f"Expected absolute GitHub URL for /resume.md in processed content, "
|
| 283 |
+
f"but got: {processed_content}"
|
| 284 |
+
)
|
| 285 |
+
assert "https://github.com/byoung/ai-me/blob/main/projects.md" in processed_content, (
|
| 286 |
+
f"Expected absolute GitHub URL for /projects.md in processed content, "
|
| 287 |
+
f"but got: {processed_content}"
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
logger.info("✓ Test passed: Relative GitHub links converted to absolute URLs")
|
| 291 |
+
logger.info(f" Original: [my resume](/resume.md)")
|
| 292 |
+
logger.info(f" Converted: [my resume](https://github.com/byoung/ai-me/blob/main/resume.md)")
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@pytest.mark.asyncio
|
| 296 |
+
async def test_agent_responses_cite_sources(ai_me_agent):
|
| 297 |
+
"""Tests FR-004, FR-011: Agent responses include source citations.
|
| 298 |
+
|
| 299 |
+
Validates that agent responses include proper source attribution,
|
| 300 |
+
which could be GitHub URLs, local paths, or explicit source references.
|
| 301 |
+
"""
|
| 302 |
+
questions = [
|
| 303 |
+
"What do you know about ReaR?",
|
| 304 |
+
"Tell me about your experience in technology",
|
| 305 |
+
]
|
| 306 |
+
|
| 307 |
+
for question in questions:
|
| 308 |
+
logger.info(f"\n{'='*60}\nSource citation test: {question}\n{'='*60}")
|
| 309 |
+
|
| 310 |
+
response = await ai_me_agent.run(question)
|
| 311 |
+
|
| 312 |
+
# Check that response includes some form of source attribution
|
| 313 |
+
# Could be: GitHub URL, local path, "Sources" section, etc.
|
| 314 |
+
has_source = (
|
| 315 |
+
"https://github.com/" in response or
|
| 316 |
+
".md" in response or # Local markdown file reference
|
| 317 |
+
"source" in response.lower() or
|
| 318 |
+
"documentation" in response.lower()
|
| 319 |
+
)
|
| 320 |
+
assert has_source, (
|
| 321 |
+
f"Expected source attribution in response to '{question}' "
|
| 322 |
+
f"but found none. Response: {response}"
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
# Verify response is substantive (not just metadata)
|
| 326 |
+
min_length = 50
|
| 327 |
+
assert len(response) > min_length, (
|
| 328 |
+
f"Response to '{question}' was too short: {response}"
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
logger.info(f"✓ Source citation found for: {question[:40]}...")
|
| 332 |
+
|
| 333 |
+
logger.info("\n✓ Test passed: Agent responses cite sources (FR-004, FR-011)")
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
@pytest.mark.asyncio
|
| 337 |
+
async def test_user_story_2_multi_topic_consistency(ai_me_agent):
|
| 338 |
+
"""
|
| 339 |
+
Tests FR-001, FR-003, FR-005, NFR-002: User Story 2 - Multi-Topic Consistency
|
| 340 |
+
|
| 341 |
+
Verify that the agent maintains consistent first-person perspective
|
| 342 |
+
across multiple conversation topics.
|
| 343 |
+
|
| 344 |
+
This tests that the agent:
|
| 345 |
+
- Uses first-person perspective (I, my, me) consistently
|
| 346 |
+
- Maintains professional tone across different topic switches
|
| 347 |
+
- Shows context awareness of different topics
|
| 348 |
+
- Remains in-character as the personified individual
|
| 349 |
+
"""
|
| 350 |
+
# Ask 3 questions about different topics
|
| 351 |
+
topics = [
|
| 352 |
+
("What is your background in technology?", "background|experience|technology"),
|
| 353 |
+
("What programming languages are you skilled in?", "programming|language|skilled"),
|
| 354 |
+
]
|
| 355 |
+
|
| 356 |
+
first_person_patterns = [
|
| 357 |
+
r"\bi\b", r"\bme\b", r"\bmy\b", r"\bmyself\b",
|
| 358 |
+
r"\bI['m]", r"\bI['ve]", r"\bI['ll]"
|
| 359 |
+
]
|
| 360 |
+
|
| 361 |
+
for question, topic_keywords in topics:
|
| 362 |
+
logger.info(f"\n{'='*60}\nMulti-topic test question: {question}\n{'='*60}")
|
| 363 |
+
|
| 364 |
+
response = await ai_me_agent.run(question)
|
| 365 |
+
response_lower = response.lower()
|
| 366 |
+
|
| 367 |
+
# Check for first-person usage
|
| 368 |
+
first_person_found = any(
|
| 369 |
+
re.search(pattern, response, re.IGNORECASE)
|
| 370 |
+
for pattern in first_person_patterns
|
| 371 |
+
)
|
| 372 |
+
assert first_person_found, (
|
| 373 |
+
f"Expected first-person perspective in response to '{question}' "
|
| 374 |
+
f"but got: {response}"
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Verify response is substantive (not just "I don't know")
|
| 378 |
+
min_length = 50 # Substantive responses should be > 50 chars
|
| 379 |
+
assert len(response) > min_length, (
|
| 380 |
+
f"Response to '{question}' was too short (likely not substantive): {response}"
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
logger.info(f"✓ First-person perspective maintained for: {question[:40]}...")
|
| 384 |
+
logger.info(f" Response preview: {response[:100]}...")
|
| 385 |
+
|
| 386 |
+
logger.info("\n✓ Test passed: Consistent first-person perspective across 3+ topics")
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
@pytest.mark.asyncio
|
| 390 |
+
async def test_tool_failure_error_messages_are_friendly(caplog, ai_me_agent):
|
| 391 |
+
"""
|
| 392 |
+
Tests FR-012, NFR-003: Error Message Quality (FR-012)
|
| 393 |
+
|
| 394 |
+
Verify that tool failures return user-friendly messages without Python tracebacks.
|
| 395 |
+
|
| 396 |
+
This tests that the agent:
|
| 397 |
+
- Returns human-readable error messages
|
| 398 |
+
- logs an error that can be reviewed in our dashboard/logs
|
| 399 |
+
|
| 400 |
+
Uses mocking to simulate tool failures without adding test-specific code to agent.py
|
| 401 |
+
"""
|
| 402 |
+
logger.info(f"\n{'='*60}\nError Handling Test\n{'='*60}")
|
| 403 |
+
|
| 404 |
+
# Mock the Runner.run method to simulate a tool failure
|
| 405 |
+
# This tests the catch-all exception handler without adding test code to production
|
| 406 |
+
test_scenarios = [
|
| 407 |
+
RuntimeError("Simulated tool timeout"),
|
| 408 |
+
ValueError("Invalid tool parameters"),
|
| 409 |
+
]
|
| 410 |
+
|
| 411 |
+
for error in test_scenarios:
|
| 412 |
+
logger.info(f"\nTesting error scenario: {error.__class__.__name__}: {error}")
|
| 413 |
+
|
| 414 |
+
# Clear previous log records for this iteration
|
| 415 |
+
caplog.clear()
|
| 416 |
+
|
| 417 |
+
# Mock Runner.run to raise an exception
|
| 418 |
+
with patch('agent.Runner.run', new_callable=AsyncMock) as mock_run:
|
| 419 |
+
mock_run.side_effect = error
|
| 420 |
+
|
| 421 |
+
response = await ai_me_agent.run("Any user question")
|
| 422 |
+
|
| 423 |
+
logger.info(f"Response: {response[:100]}...")
|
| 424 |
+
|
| 425 |
+
# PRIMARY CHECK: Verify "I encountered an unexpected error" is in response
|
| 426 |
+
assert "I encountered an unexpected error" in response, (
|
| 427 |
+
f"Response must contain 'I encountered an unexpected error'. Got: {response}"
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# SECONDARY CHECK: Verify error was logged by agent.py
|
| 431 |
+
error_logs = [record for record in caplog.records if record.levelname == "ERROR"]
|
| 432 |
+
assert len(error_logs) > 0, "Expected at least one ERROR log record from agent.py"
|
| 433 |
+
|
| 434 |
+
# Find the agent.py error log (contains "Unexpected error:")
|
| 435 |
+
agent_error_logged = any(
|
| 436 |
+
"Unexpected error:" in record.message for record in error_logs
|
| 437 |
+
)
|
| 438 |
+
assert agent_error_logged, (
|
| 439 |
+
f"Expected ERROR log with 'Unexpected error:' from agent.py. "
|
| 440 |
+
f"Got: {[r.message for r in error_logs]}"
|
| 441 |
+
)
|
| 442 |
+
error_messages = [
|
| 443 |
+
r.message for r in error_logs
|
| 444 |
+
if "Unexpected error:" in r.message
|
| 445 |
+
]
|
| 446 |
+
logger.info(
|
| 447 |
+
f"✓ Error properly logged to logger: {error_messages}"
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
logger.info("\n✓ Test passed: Error messages are friendly (FR-012) + properly logged")
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
@pytest.mark.asyncio
|
| 454 |
+
async def test_logger_setup_format(caplog):
|
| 455 |
+
"""Tests NFR-003 (Structured Logging): Verify setup_logger creates structured logging.
|
| 456 |
+
|
| 457 |
+
Tests that setup_logger() configures syslog-style format with JSON support for
|
| 458 |
+
structured logging of user/agent interactions.
|
| 459 |
+
|
| 460 |
+
This validates the logger configuration that our production app relies on
|
| 461 |
+
for analytics and debugging.
|
| 462 |
+
"""
|
| 463 |
+
# Force logger setup to run by clearing handlers so setup_logger reconfigures
|
| 464 |
+
root_logger = logging.getLogger()
|
| 465 |
+
original_handlers = root_logger.handlers[:]
|
| 466 |
+
for handler in root_logger.handlers[:]:
|
| 467 |
+
root_logger.removeHandler(handler)
|
| 468 |
+
|
| 469 |
+
try:
|
| 470 |
+
# Now call setup_logger with no handlers - should trigger full setup
|
| 471 |
+
test_logger = setup_logger("test.structured_logging")
|
| 472 |
+
|
| 473 |
+
# Verify logger was created
|
| 474 |
+
assert test_logger.name == "test.structured_logging"
|
| 475 |
+
|
| 476 |
+
# Verify root logger now has handlers (setup_logger should have added them)
|
| 477 |
+
assert len(root_logger.handlers) > 0, (
|
| 478 |
+
"Root logger should have handlers after setup_logger"
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
# Verify we have a StreamHandler (console output)
|
| 482 |
+
has_stream_handler = any(
|
| 483 |
+
isinstance(handler, logging.StreamHandler)
|
| 484 |
+
for handler in root_logger.handlers
|
| 485 |
+
)
|
| 486 |
+
assert has_stream_handler, "Should have StreamHandler for console output"
|
| 487 |
+
|
| 488 |
+
# Test that logging works with structured JSON format
|
| 489 |
+
# The formatters should support JSON logging for analytics
|
| 490 |
+
test_logger.info(
|
| 491 |
+
'{"session_id": "test-session", "user_input": "test message"}'
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
logger.info(
|
| 495 |
+
"✓ Test passed: Logger setup configures structured logging (NFR-003)"
|
| 496 |
+
)
|
| 497 |
+
finally:
|
| 498 |
+
# Restore original handlers
|
| 499 |
+
for handler in root_logger.handlers[:]:
|
| 500 |
+
root_logger.removeHandler(handler)
|
| 501 |
+
for handler in original_handlers:
|
| 502 |
+
root_logger.addHandler(handler)
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
if __name__ == "__main__":
|
| 506 |
+
# Allow running tests directly with python test.py
|
| 507 |
+
pytest.main([__file__, "-v", "-s"])
|
tests/unit/test_config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for config.py Config and DataManagerConfig classes.
|
| 3 |
+
|
| 4 |
+
Tests configuration validation, Pydantic models, and environment variable parsing
|
| 5 |
+
in isolation without requiring full application setup.
|
| 6 |
+
"""
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
from config import Config
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_config_github_repos_parsing():
|
| 13 |
+
"""Tests NFR-002 (Type-Safe Configuration): Config.parse_github_repos validator.
|
| 14 |
+
|
| 15 |
+
Validates that the field validator correctly parses comma-separated repository
|
| 16 |
+
strings from environment variables, including edge cases like empty strings and
|
| 17 |
+
pre-parsed lists. Ensures configuration is validated via Pydantic with strict
|
| 18 |
+
typing and no silent failures.
|
| 19 |
+
"""
|
| 20 |
+
# Test empty string
|
| 21 |
+
result = Config.parse_github_repos("")
|
| 22 |
+
assert result == [], "Empty string should parse to empty list"
|
| 23 |
+
|
| 24 |
+
# Test single repo
|
| 25 |
+
result = Config.parse_github_repos("owner/repo")
|
| 26 |
+
assert result == ["owner/repo"], "Single repo should parse correctly"
|
| 27 |
+
|
| 28 |
+
# Test multiple repos with spaces
|
| 29 |
+
result = Config.parse_github_repos("owner1/repo1, owner2/repo2 , owner3/repo3")
|
| 30 |
+
assert result == ["owner1/repo1", "owner2/repo2", "owner3/repo3"], (
|
| 31 |
+
"Multiple repos with spaces should parse and strip correctly"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Test already a list
|
| 35 |
+
result = Config.parse_github_repos(["owner/repo"])
|
| 36 |
+
assert result == ["owner/repo"], "Already a list should pass through"
|
tests/unit/test_data.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for data.py DataManager class.
|
| 3 |
+
|
| 4 |
+
Tests individual methods of the DataManager and DataManagerConfig in isolation,
|
| 5 |
+
without requiring external APIs or full integration setup.
|
| 6 |
+
"""
|
| 7 |
+
import pytest
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from unittest.mock import patch, MagicMock
|
| 11 |
+
from langchain_core.documents import Document
|
| 12 |
+
|
| 13 |
+
from data import DataManager, DataManagerConfig
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestLoadLocalDocuments:
|
| 17 |
+
"""Tests for DataManager.load_local_documents() method.
|
| 18 |
+
|
| 19 |
+
Implements FR-002 (Knowledge Retrieval): Document loading from local filesystem.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def test_load_local_documents_missing_directory(self):
|
| 23 |
+
"""Tests FR-002: Handle missing doc_root gracefully.
|
| 24 |
+
|
| 25 |
+
When doc_root directory doesn't exist, load_local_documents should
|
| 26 |
+
return empty list and log warning instead of raising exception.
|
| 27 |
+
"""
|
| 28 |
+
# Create config pointing to non-existent directory
|
| 29 |
+
config = DataManagerConfig(doc_root="/nonexistent/path/xyz")
|
| 30 |
+
dm = DataManager(config=config)
|
| 31 |
+
|
| 32 |
+
# Should return empty list, not raise exception
|
| 33 |
+
docs = dm.load_local_documents()
|
| 34 |
+
|
| 35 |
+
assert docs == [], "Expected empty list for missing directory"
|
| 36 |
+
assert isinstance(docs, list), "Expected list return type"
|
| 37 |
+
|
| 38 |
+
def test_load_local_documents_valid_directory(self):
|
| 39 |
+
"""Tests FR-002: Load documents from existing directory.
|
| 40 |
+
|
| 41 |
+
When doc_root exists, load_local_documents should return loaded documents.
|
| 42 |
+
Uses tests/data directory which contains sample markdown files.
|
| 43 |
+
"""
|
| 44 |
+
# Use test data directory
|
| 45 |
+
test_data_dir = str(Path(__file__).parent.parent / "data")
|
| 46 |
+
config = DataManagerConfig(doc_root=test_data_dir)
|
| 47 |
+
dm = DataManager(config=config)
|
| 48 |
+
|
| 49 |
+
# Should load documents
|
| 50 |
+
docs = dm.load_local_documents()
|
| 51 |
+
|
| 52 |
+
assert isinstance(docs, list), "Expected list return type"
|
| 53 |
+
assert len(docs) > 0, "Expected to find documents in tests/data"
|
| 54 |
+
|
| 55 |
+
# Verify documents have required metadata
|
| 56 |
+
for doc in docs:
|
| 57 |
+
assert "source" in doc.metadata, "Document should have source metadata"
|
| 58 |
+
assert doc.page_content, "Document should have content"
|
| 59 |
+
|
| 60 |
+
def test_load_local_documents_multiple_glob_patterns(self):
|
| 61 |
+
"""Tests FR-002: Load documents using multiple glob patterns (lines 81-83).
|
| 62 |
+
|
| 63 |
+
Tests the for loop iteration over multiple glob patterns in load_local_documents.
|
| 64 |
+
This covers lines 81-83 where patterns are iterated and loaded.
|
| 65 |
+
"""
|
| 66 |
+
# Use test data directory
|
| 67 |
+
test_data_dir = str(Path(__file__).parent.parent / "data")
|
| 68 |
+
|
| 69 |
+
# Create config with multiple glob patterns
|
| 70 |
+
config = DataManagerConfig(
|
| 71 |
+
doc_root=test_data_dir,
|
| 72 |
+
doc_load_local=["*.md", "**/*.md"] # Multiple patterns
|
| 73 |
+
)
|
| 74 |
+
dm = DataManager(config=config)
|
| 75 |
+
|
| 76 |
+
# Should load documents from all patterns
|
| 77 |
+
docs = dm.load_local_documents()
|
| 78 |
+
|
| 79 |
+
assert isinstance(docs, list), "Expected list return type"
|
| 80 |
+
assert len(docs) > 0, "Expected to find documents with multiple patterns"
|
| 81 |
+
|
| 82 |
+
# Verify all patterns were processed (should have more docs due to overlap)
|
| 83 |
+
assert len(docs) >= 3, "Expected at least 3 docs from test data"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class TestProcessDocuments:
|
| 87 |
+
"""Tests for DataManager.process_documents() method.
|
| 88 |
+
|
| 89 |
+
Implements FR-004 (Source Attribution): Converting relative GitHub links
|
| 90 |
+
to absolute URLs in markdown documents.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def test_process_documents_converts_relative_links_to_absolute(self):
|
| 94 |
+
"""Tests FR-004: Relative GitHub links converted to absolute URLs.
|
| 95 |
+
|
| 96 |
+
Verifies that process_documents rewrites relative links like /path/file.md
|
| 97 |
+
to absolute GitHub URLs like https://github.com/owner/repo/blob/main/path/file.md
|
| 98 |
+
"""
|
| 99 |
+
# Create a sample document with relative GitHub links
|
| 100 |
+
sample_doc = Document(
|
| 101 |
+
page_content=(
|
| 102 |
+
"Check out [my resume](/resume.md) and "
|
| 103 |
+
"[projects](/projects.md) for more info."
|
| 104 |
+
),
|
| 105 |
+
metadata={
|
| 106 |
+
"source": "github://byoung/ai-me/docs/about.md",
|
| 107 |
+
"github_repo": "byoung/ai-me"
|
| 108 |
+
}
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
config = DataManagerConfig()
|
| 112 |
+
dm = DataManager(config=config)
|
| 113 |
+
|
| 114 |
+
# Process the document
|
| 115 |
+
processed_docs = dm.process_documents([sample_doc])
|
| 116 |
+
|
| 117 |
+
assert len(processed_docs) == 1, "Expected 1 processed document"
|
| 118 |
+
processed_content = processed_docs[0].page_content
|
| 119 |
+
|
| 120 |
+
# Verify relative links were converted to absolute GitHub URLs
|
| 121 |
+
assert "https://github.com/byoung/ai-me/blob/main/resume.md" in processed_content, (
|
| 122 |
+
f"Expected absolute URL for /resume.md in: {processed_content}"
|
| 123 |
+
)
|
| 124 |
+
assert "https://github.com/byoung/ai-me/blob/main/projects.md" in processed_content, (
|
| 125 |
+
f"Expected absolute URL for /projects.md in: {processed_content}"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def test_process_documents_preserves_non_github_docs(self):
|
| 129 |
+
"""Tests FR-004: Non-GitHub documents are preserved unchanged.
|
| 130 |
+
|
| 131 |
+
Documents without github_repo metadata should pass through unchanged.
|
| 132 |
+
"""
|
| 133 |
+
# Create a document without github_repo metadata
|
| 134 |
+
sample_doc = Document(
|
| 135 |
+
page_content="[my resume](/resume.md)",
|
| 136 |
+
metadata={
|
| 137 |
+
"source": "local://docs/about.md"
|
| 138 |
+
}
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
config = DataManagerConfig()
|
| 142 |
+
dm = DataManager(config=config)
|
| 143 |
+
|
| 144 |
+
# Process the document
|
| 145 |
+
processed_docs = dm.process_documents([sample_doc])
|
| 146 |
+
|
| 147 |
+
assert len(processed_docs) == 1, "Expected 1 processed document"
|
| 148 |
+
# Content should be unchanged (no github_repo in metadata)
|
| 149 |
+
assert processed_docs[0].page_content == "[my resume](/resume.md)", (
|
| 150 |
+
"Non-GitHub document should not be modified"
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
def test_process_documents_handles_markdown_with_anchors(self):
|
| 154 |
+
"""Tests FR-004: Markdown links with anchor fragments are preserved.
|
| 155 |
+
|
| 156 |
+
Links like [text](/file.md#section) should preserve the anchor in the URL.
|
| 157 |
+
"""
|
| 158 |
+
sample_doc = Document(
|
| 159 |
+
page_content="See [section](/docs/guide.md#installation) for details.",
|
| 160 |
+
metadata={
|
| 161 |
+
"source": "github://user/repo/README.md",
|
| 162 |
+
"github_repo": "user/repo"
|
| 163 |
+
}
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
config = DataManagerConfig()
|
| 167 |
+
dm = DataManager(config=config)
|
| 168 |
+
|
| 169 |
+
processed_docs = dm.process_documents([sample_doc])
|
| 170 |
+
processed_content = processed_docs[0].page_content
|
| 171 |
+
|
| 172 |
+
# Verify anchor is preserved in the URL
|
| 173 |
+
assert "https://github.com/user/repo/blob/main/docs/guide.md#installation" in processed_content, (
|
| 174 |
+
f"Expected anchor preserved in URL in: {processed_content}"
|
| 175 |
+
)
|
uv.lock
CHANGED
|
@@ -5,7 +5,7 @@ requires-python = "==3.12.*"
|
|
| 5 |
[[package]]
|
| 6 |
name = "ai-me"
|
| 7 |
version = "0.1.0"
|
| 8 |
-
source = {
|
| 9 |
dependencies = [
|
| 10 |
{ name = "chromadb" },
|
| 11 |
{ name = "fastmcp" },
|
|
@@ -36,6 +36,7 @@ dev = [
|
|
| 36 |
{ name = "ipywidgets" },
|
| 37 |
{ name = "pytest" },
|
| 38 |
{ name = "pytest-asyncio" },
|
|
|
|
| 39 |
]
|
| 40 |
|
| 41 |
[package.metadata]
|
|
@@ -69,6 +70,7 @@ dev = [
|
|
| 69 |
{ name = "ipywidgets", specifier = "~=8.1" },
|
| 70 |
{ name = "pytest", specifier = "~=8.0" },
|
| 71 |
{ name = "pytest-asyncio", specifier = "~=0.24" },
|
|
|
|
| 72 |
]
|
| 73 |
|
| 74 |
[[package]]
|
|
@@ -418,6 +420,28 @@ wheels = [
|
|
| 418 |
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
| 419 |
]
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
[[package]]
|
| 422 |
name = "cryptography"
|
| 423 |
version = "46.0.2"
|
|
@@ -2331,6 +2355,20 @@ wheels = [
|
|
| 2331 |
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
|
| 2332 |
]
|
| 2333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2334 |
[[package]]
|
| 2335 |
name = "python-dateutil"
|
| 2336 |
version = "2.9.0.post0"
|
|
|
|
| 5 |
[[package]]
|
| 6 |
name = "ai-me"
|
| 7 |
version = "0.1.0"
|
| 8 |
+
source = { editable = "." }
|
| 9 |
dependencies = [
|
| 10 |
{ name = "chromadb" },
|
| 11 |
{ name = "fastmcp" },
|
|
|
|
| 36 |
{ name = "ipywidgets" },
|
| 37 |
{ name = "pytest" },
|
| 38 |
{ name = "pytest-asyncio" },
|
| 39 |
+
{ name = "pytest-cov" },
|
| 40 |
]
|
| 41 |
|
| 42 |
[package.metadata]
|
|
|
|
| 70 |
{ name = "ipywidgets", specifier = "~=8.1" },
|
| 71 |
{ name = "pytest", specifier = "~=8.0" },
|
| 72 |
{ name = "pytest-asyncio", specifier = "~=0.24" },
|
| 73 |
+
{ name = "pytest-cov", specifier = "~=6.0" },
|
| 74 |
]
|
| 75 |
|
| 76 |
[[package]]
|
|
|
|
| 420 |
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
| 421 |
]
|
| 422 |
|
| 423 |
+
[[package]]
|
| 424 |
+
name = "coverage"
|
| 425 |
+
version = "7.11.0"
|
| 426 |
+
source = { registry = "https://pypi.org/simple" }
|
| 427 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" }
|
| 428 |
+
wheels = [
|
| 429 |
+
{ url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" },
|
| 430 |
+
{ url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" },
|
| 431 |
+
{ url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" },
|
| 432 |
+
{ url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" },
|
| 433 |
+
{ url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" },
|
| 434 |
+
{ url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" },
|
| 435 |
+
{ url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" },
|
| 436 |
+
{ url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" },
|
| 437 |
+
{ url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" },
|
| 438 |
+
{ url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" },
|
| 439 |
+
{ url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" },
|
| 440 |
+
{ url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" },
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" },
|
| 442 |
+
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
|
| 443 |
+
]
|
| 444 |
+
|
| 445 |
[[package]]
|
| 446 |
name = "cryptography"
|
| 447 |
version = "46.0.2"
|
|
|
|
| 2355 |
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
|
| 2356 |
]
|
| 2357 |
|
| 2358 |
+
[[package]]
|
| 2359 |
+
name = "pytest-cov"
|
| 2360 |
+
version = "6.3.0"
|
| 2361 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2362 |
+
dependencies = [
|
| 2363 |
+
{ name = "coverage" },
|
| 2364 |
+
{ name = "pluggy" },
|
| 2365 |
+
{ name = "pytest" },
|
| 2366 |
+
]
|
| 2367 |
+
sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" }
|
| 2368 |
+
wheels = [
|
| 2369 |
+
{ url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" },
|
| 2370 |
+
]
|
| 2371 |
+
|
| 2372 |
[[package]]
|
| 2373 |
name = "python-dateutil"
|
| 2374 |
version = "2.9.0.post0"
|