diff --git a/README.md b/README.md index a97cc58..036550d 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,12 @@ Copy `.env.example` to `.env` and set your API credentials: COGSOL_ENV=local COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ -COGSOL_API_TOKEN=your-api-token +COGSOL_ +COGSOL_API_KEY=your-api-key +# Optional: Azure AD B2C client credentials for JWT +# If not provided, the Auth will be skipped +COGSOL_AUTH_CLIENT_ID=your-client-id +COGSOL_AUTH_SECRET=your-client-secret ``` ### 4. Create Migrations @@ -667,8 +672,15 @@ from cogsol.content import BaseRetrieval, ReorderingStrategy |----------|----------|-------------| | `COGSOL_API_BASE` | Yes | Base URL for the CogSol Cognitive API | | `COGSOL_CONTENT_API_BASE` | No | Base URL for the CogSol Content API (defaults to `COGSOL_API_BASE`) | -| `COGSOL_API_TOKEN` | Yes | API authentication token | +| `COGSOL_API_KEY` | Yes | API Key authentication | | `COGSOL_ENV` | No | Environment name (e.g., `local`, `production`) | +| `COGSOL_AUTH_CLIENT_ID` | No | Client Id provided for adminitrators | +| `COGSOL_AUTH_SECRET` | No | Auth Secret provided for adminitrators | + +# Optional: Azure AD B2C client credentials for JWT\ +# If not provided, the Auth will be skipped +COGSOL_AUTH_CLIENT_ID=your-client-id +COGSOL_AUTH_SECRET=your-client-secret ### Project Settings (`settings.py`) diff --git a/cogsol/agents/__init__.py b/cogsol/agents/__init__.py index c328191..0245c2d 100644 --- a/cogsol/agents/__init__.py +++ b/cogsol/agents/__init__.py @@ -10,7 +10,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Any, Optional +from typing import Any from cogsol.core.api import CogSolAPIError, CogSolClient from cogsol.core.env import load_dotenv @@ -23,18 +23,18 @@ class BaseAgent: """ system_prompt: Any = None - initial_message: Optional[str] = None - forced_termination_message: Optional[str] = None - no_information_message: Optional[str] = None + initial_message: str | None = None + forced_termination_message: str | None = None + no_information_message: str | None = None pregeneration_config: Any = None generation_config: Any = None pretools: list[Any] = [] tools: list[Any] = [] - temperature: Optional[float] = None - max_interactions: Optional[int] = None - user_message_length: Optional[int] = None - consecutive_tool_calls_limit: Optional[int] = None - user_interactions_window: Optional[int] = None + temperature: float | None = None + max_interactions: int | None = None + user_message_length: int | None = None + consecutive_tool_calls_limit: int | None = None + user_interactions_window: int | None = None token_optimization: Any = None streaming: bool = False self_improvement_mode: bool = False @@ -44,27 +44,27 @@ class BaseAgent: fixed_responses: list[Any] = [] class Meta: - name: Optional[str] = None - chat_name: Optional[str] = None - logo_url: Optional[str] = None - assistant_name_color: Optional[str] = None - primary_color: Optional[str] = None - secondary_color: Optional[str] = None - border_color: Optional[str] = None + name: str | None = None + chat_name: str | None = None + logo_url: str | None = None + assistant_name_color: str | None = None + primary_color: str | None = None + secondary_color: str | None = None + border_color: str | None = None def __init__( self, *, - assistant_id: Optional[int] = None, - chat_id: Optional[int] = None, - api_base: Optional[str] = None, - api_token: Optional[str] = None, - project_path: Optional[Path] = None, + assistant_id: int | None = None, + chat_id: int | None = None, + api_base: str | None = None, + api_key: str | None = None, + project_path: Path | None = None, ) -> None: self._assistant_id = assistant_id self._chat_id = chat_id self._api_base = api_base - self._api_token = api_token + self._api_key = api_key self._project_path = project_path def reset(self) -> None: @@ -76,10 +76,10 @@ def run( message: str, *, reset: bool = False, - assistant_id: Optional[int] = None, - api_base: Optional[str] = None, - api_token: Optional[str] = None, - project_path: Optional[Path] = None, + assistant_id: int | None = None, + api_base: str | None = None, + api_key: str | None = None, + project_path: Path | None = None, async_mode: bool = False, streaming: bool = False, **params: Any, @@ -101,7 +101,7 @@ def run( base_url = api_base or self._api_base or os.environ.get("COGSOL_API_BASE") if not base_url: raise CogSolAPIError("COGSOL_API_BASE is required to run agents.") - token = api_token or self._api_token or os.environ.get("COGSOL_API_TOKEN") + token = api_key or self._api_key or os.environ.get("COGSOL_API_KEY") assistant_id = assistant_id or self._assistant_id if assistant_id is None: @@ -118,7 +118,7 @@ def run( continue payload[key] = self._normalize_payload_value(value) - client = CogSolClient(base_url, token=token) + client = CogSolClient(base_url, api_key=token) if self._chat_id is None: chat = client.create_chat( assistant_id, @@ -155,7 +155,7 @@ def definition(cls) -> dict[str, Any]: }, } - def _resolve_project_path(self, project_path: Optional[Path]) -> Optional[Path]: + def _resolve_project_path(self, project_path: Path | None) -> Path | None: if project_path is not None: return project_path if self._project_path is not None: @@ -169,7 +169,7 @@ def _resolve_project_path(self, project_path: Optional[Path]) -> Optional[Path]: return parent.parent return None - def _resolve_assistant_id(self, project_path: Optional[Path]) -> Optional[int]: + def _resolve_assistant_id(self, project_path: Path | None) -> int | None: if project_path is None: project_path = Path.cwd() state_path = project_path / "agents" / "migrations" / ".state.json" @@ -192,7 +192,7 @@ def _resolve_assistant_id(self, project_path: Optional[Path]) -> Optional[int]: return int(value) return None - def _chat_id_from_response(self, chat_obj: Any) -> Optional[int]: + def _chat_id_from_response(self, chat_obj: Any) -> int | None: if isinstance(chat_obj, dict): value = chat_obj.get("id") if isinstance(value, int): diff --git a/cogsol/content/__init__.py b/cogsol/content/__init__.py index 6cc128b..63d3985 100644 --- a/cogsol/content/__init__.py +++ b/cogsol/content/__init__.py @@ -12,7 +12,7 @@ import sys from enum import Enum from pathlib import Path -from typing import Any, Optional +from typing import Any from cogsol.core.api import CogSolAPIError, CogSolClient from cogsol.core.env import load_dotenv @@ -89,7 +89,7 @@ class Meta: delete_orphaned_metadata: bool = False class Meta: - description: Optional[str] = None + description: str | None = None def __repr__(self) -> str: return f"" @@ -121,8 +121,8 @@ class AuthorMetadata(BaseMetadataConfig): # Value constraints possible_values: list[str] = [] - default_value: Optional[str] = None - format: Optional[str] = None # Required for DATE type (e.g., "YYYY-MM-DD") + default_value: str | None = None + format: str | None = None # Required for DATE type (e.g., "YYYY-MM-DD") # Behavior flags filtrable: bool = False @@ -176,7 +176,7 @@ class StandardPDFIngestion(BaseIngestionConfig): """ name: str - default_topic: Optional[type[BaseTopic]] = None + default_topic: type[BaseTopic] | None = None # PDF parsing options pdf_parsing_mode: PDFParsingMode = PDFParsingMode.BOTH @@ -221,7 +221,7 @@ class ProductDocsRetrieval(BaseRetrieval): """ name: str - topic: Optional[type[BaseTopic]] = None + topic: type[BaseTopic] | None = None # Result configuration num_refs: int = 10 @@ -229,9 +229,9 @@ class ProductDocsRetrieval(BaseRetrieval): # Reordering options reordering: bool = False - strategy_reordering: Optional[ReorderingStrategy] = None + strategy_reordering: ReorderingStrategy | None = None retrieval_window: int = 20 - reordering_metadata: Optional[str] = None + reordering_metadata: str | None = None fixed_blocks_reordering: int = 3 # Context blocks @@ -251,15 +251,15 @@ class ProductDocsRetrieval(BaseRetrieval): def __init__( self, *, - retrieval_id: Optional[int] = None, - api_base: Optional[str] = None, - api_token: Optional[str] = None, - content_api_base: Optional[str] = None, - project_path: Optional[Path] = None, + retrieval_id: int | None = None, + api_base: str | None = None, + api_key: str | None = None, + content_api_base: str | None = None, + project_path: Path | None = None, ) -> None: self._retrieval_id = retrieval_id self._api_base = api_base - self._api_token = api_token + self._api_key = api_key self._content_api_base = content_api_base self._project_path = project_path @@ -267,12 +267,12 @@ def run( self, question: str, *, - doc_type: Optional[str | DocType] = None, - retrieval_id: Optional[int] = None, - api_base: Optional[str] = None, - api_token: Optional[str] = None, - content_api_base: Optional[str] = None, - project_path: Optional[Path] = None, + doc_type: str | DocType | None = None, + retrieval_id: int | None = None, + api_base: str | None = None, + api_key: str | None = None, + content_api_base: str | None = None, + project_path: Path | None = None, **params: Any, ) -> Any: """ @@ -292,7 +292,7 @@ def run( base_url = api_base or self._api_base or os.environ.get("COGSOL_API_BASE") or content_base if not content_base: raise CogSolAPIError("COGSOL_CONTENT_API_BASE is required to run retrievals.") - token = api_token or self._api_token or os.environ.get("COGSOL_API_TOKEN") + token = api_key or self._api_key or os.environ.get("COGSOL_API_KEY") retrieval_id = retrieval_id or self._retrieval_id if retrieval_id is None: @@ -312,7 +312,7 @@ def run( payload[key] = self._normalize_payload_value(value) if base_url: - client = CogSolClient(base_url, token=token, content_base_url=content_base) + client = CogSolClient(base_url, api_key=token, content_base_url=content_base) else: raise CogSolAPIError("COGSOL_CONTENT_API_BASE is required to run retrievals.") return client.request( @@ -322,7 +322,7 @@ def run( use_content_api=True, ) - def _resolve_project_path(self, project_path: Optional[Path]) -> Optional[Path]: + def _resolve_project_path(self, project_path: Path | None) -> Path | None: if project_path is not None: return project_path if self._project_path is not None: @@ -336,7 +336,7 @@ def _resolve_project_path(self, project_path: Optional[Path]) -> Optional[Path]: return parent.parent return None - def _resolve_retrieval_id(self, project_path: Optional[Path]) -> Optional[int]: + def _resolve_retrieval_id(self, project_path: Path | None) -> int | None: if project_path is None: project_path = Path.cwd() state_path = project_path / "data" / "migrations" / ".state.json" diff --git a/cogsol/core/api.py b/cogsol/core/api.py index c9e6c3e..0dc2971 100644 --- a/cogsol/core/api.py +++ b/cogsol/core/api.py @@ -2,12 +2,17 @@ import json import mimetypes +import os +import time import uuid from dataclasses import dataclass from pathlib import Path -from typing import Any, Optional +from typing import Any from urllib import error, request +import msal +from jwt import decode + class CogSolAPIError(RuntimeError): pass @@ -57,8 +62,25 @@ def _create_multipart_data(fields: dict[str, Any], files: dict[str, Path]) -> tu @dataclass class CogSolClient: base_url: str - token: Optional[str] = None - content_base_url: Optional[str] = None # Separate URL for Content API + api_key: str | None = None + bearer_token: str | None = None + bearer_token_expires_at: float | None = None + content_base_url: str | None = None # Separate URL for Content API + + def __init__( + self, + base_url: str | None = None, + api_key: str | None = None, + content_base_url: str | None = None, + ) -> None: + self.base_url = base_url or os.environ.get("COGSOL_API_BASE") or "" + self.api_key = api_key or os.environ.get("COGSOL_API_KEY") + self.content_base_url = content_base_url or os.environ.get("COGSOL_CONTENT_API_BASE") + self.bearer_token = None + self.bearer_token_expires_at = None + + if self._bearer_configured(): + self._ensure_bearer_token() def _url(self, path: str, use_content_api: bool = False) -> str: if path.startswith("http://") or path.startswith("https://"): @@ -66,34 +88,79 @@ def _url(self, path: str, use_content_api: bool = False) -> str: base = self.content_base_url if use_content_api and self.content_base_url else self.base_url return f"{base.rstrip('/')}/{path.lstrip('/')}" - def _headers(self) -> dict[str, str]: - headers = {"Content-Type": "application/json"} - if self.token: - headers["x-api-key"] = f"{self.token}" + def _headers(self, content_type: str) -> dict[str, str]: + headers = {"Content-Type": content_type} + if self.api_key: + headers["x-api-key"] = f"{self.api_key}" + if self.bearer_token: + headers["Authorization"] = f"Bearer {self.bearer_token}" return headers + def _bearer_configured(self) -> bool: + if self.bearer_token: + return True + return bool(os.environ.get("COGSOL_AUTH_CLIENT_ID")) + + def _is_bearer_token_expired(self) -> bool: + if not self.bearer_token_expires_at: + return False + return time.time() >= self.bearer_token_expires_at + + def _set_bearer_expiry_from_jwt(self, token: str) -> None: + try: + decoded = decode(token, options={"verify_signature": False}) + except Exception: + return + exp = decoded.get("exp") + if isinstance(exp, (int, float)): + self.bearer_token_expires_at = float(exp) - 60.0 + + def _ensure_bearer_token(self, force: bool = False) -> None: + """Allows to force the token refresh when the expiry date is ok but + the token is no longer valid.""" + if self.bearer_token and not self._is_bearer_token_expired() and not force: + return + if not os.environ.get("COGSOL_AUTH_CLIENT_ID"): + return + self._refresh_bearer_token() + + def _refresh_bearer_token(self) -> None: + client_id = os.environ.get("COGSOL_AUTH_CLIENT_ID") + client_secret = os.environ.get("COGSOL_AUTH_SECRET") + + if not client_secret: + raise CogSolAPIError("Missing authentication configuration: COGSOL_AUTH_SECRET") + + authority = "https://pyxiscognitivesweden.b2clogin.com/pyxiscognitivesweden.onmicrosoft.com/B2C_1A_CS_signup_signin_Sweden_MigrationOIDC" + scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{client_id}/.default"] + + app = msal.ConfidentialClientApplication( + client_id, + authority=authority, + client_credential=client_secret, + ) + result = app.acquire_token_for_client(scopes=scopes) + + access_token = result.get("access_token") if isinstance(result, dict) else None + if not access_token: + error_code = result.get("error") if isinstance(result, dict) else None + error_desc = result.get("error_description") if isinstance(result, dict) else None + detail = f"{error_code}: {error_desc}".strip(": ") + raise CogSolAPIError(f"Token response missing access_token: {detail or result}") + self.bearer_token = access_token + self._set_bearer_expiry_from_jwt(access_token) + def request( self, method: str, path: str, - payload: Optional[dict[str, Any]] = None, + payload: dict[str, Any] | None = None, use_content_api: bool = False, ) -> Any: body = None if payload is not None: body = json.dumps(payload).encode("utf-8") - req = request.Request( - self._url(path, use_content_api), data=body, headers=self._headers(), method=method - ) - try: - with request.urlopen(req) as resp: - raw = resp.read().decode("utf-8") - return json.loads(raw) if raw else None - except error.HTTPError as exc: # pragma: no cover - I/O - detail = exc.read().decode("utf-8", errors="ignore") - raise CogSolAPIError(f"{exc.code} {exc.reason}: {detail}") from exc - except error.URLError as exc: # pragma: no cover - I/O - raise CogSolAPIError(f"Connection error: {exc.reason}") from exc + return self._request_with_retry(method, path, body=body, use_content_api=use_content_api) def request_multipart( self, @@ -105,10 +172,26 @@ def request_multipart( ) -> Any: """Send a multipart/form-data request for file uploads.""" body, content_type = _create_multipart_data(fields, files) - headers = {"Content-Type": content_type} - if self.token: - headers["x-api-key"] = f"{self.token}" + return self._request_with_retry( + method, + path, + body=body, + use_content_api=use_content_api, + content_type=content_type, + ) + def _request_with_retry( + self, + method: str, + path: str, + *, + body: bytes | None, + use_content_api: bool, + content_type: str = "application/json", + retry_on_401: bool = True, + ) -> Any: + self._ensure_bearer_token(force=not retry_on_401) # if we are retrying then force refresh + headers = self._headers(content_type=content_type) req = request.Request( self._url(path, use_content_api), data=body, headers=headers, method=method ) @@ -117,6 +200,15 @@ def request_multipart( raw = resp.read().decode("utf-8") return json.loads(raw) if raw else None except error.HTTPError as exc: # pragma: no cover - I/O + if exc.code == 401 and retry_on_401 and self._bearer_configured(): + return self._request_with_retry( + method, + path, + body=body, + use_content_api=use_content_api, + content_type=content_type, + retry_on_401=False, + ) detail = exc.read().decode("utf-8", errors="ignore") raise CogSolAPIError(f"{exc.code} {exc.reason}: {detail}") from exc except error.URLError as exc: # pragma: no cover - I/O @@ -128,14 +220,14 @@ def _ensure_id(self, data: Any, label: str) -> int: raise CogSolAPIError(f"{label} response did not include an id: {data}") return int(data["id"]) - def upsert_script(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_script(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: if remote_id: data = self.request("PUT", f"/tools/scripts/{remote_id}/", payload) else: data = self.request("POST", "/tools/scripts/", payload) return self._ensure_id(data, "Script tool") - def upsert_assistant(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_assistant(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: if remote_id: data = self.request("PUT", f"/assistants/{remote_id}/", payload) else: @@ -143,7 +235,7 @@ def upsert_assistant(self, *, remote_id: Optional[int], payload: dict[str, Any]) return self._ensure_id(data, "Assistant") def upsert_common_question( - self, *, assistant_id: int, remote_id: Optional[int], payload: dict[str, Any] + self, *, assistant_id: int, remote_id: int | None, payload: dict[str, Any] ) -> int: if remote_id: data = self.request( @@ -168,7 +260,7 @@ def upsert_common_question( raise CogSolAPIError(f"FAQ response did not include an id: {data}") def upsert_fixed_response( - self, *, assistant_id: int, remote_id: Optional[int], payload: dict[str, Any] + self, *, assistant_id: int, remote_id: int | None, payload: dict[str, Any] ) -> int: if remote_id: data = self.request( @@ -194,7 +286,7 @@ def upsert_fixed_response( raise CogSolAPIError(f"Fixed response did not include an id: {data}") def upsert_lesson( - self, *, assistant_id: int, remote_id: Optional[int], payload: dict[str, Any] + self, *, assistant_id: int, remote_id: int | None, payload: dict[str, Any] ) -> int: if remote_id: data = self.request( @@ -221,9 +313,9 @@ def upsert_lesson( def create_chat( self, assistant_id: int, - message: Optional[str] = None, + message: str | None = None, *, - payload: Optional[dict[str, Any]] = None, + payload: dict[str, Any] | None = None, async_mode: bool = False, streaming: bool = False, ) -> Any: @@ -240,9 +332,9 @@ def create_chat( def send_message( self, chat_id: int, - message: Optional[str] = None, + message: str | None = None, *, - payload: Optional[dict[str, Any]] = None, + payload: dict[str, Any] | None = None, async_mode: bool = False, streaming: bool = False, ) -> Any: @@ -265,7 +357,7 @@ def get_chat(self, chat_id: int) -> Any: def delete_script(self, script_id: int) -> None: self.request("DELETE", f"/tools/scripts/{script_id}/") - def upsert_retrieval_tool(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_retrieval_tool(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: if remote_id: data = self.request("PUT", f"/tools/retrievals/{remote_id}/", payload) else: @@ -323,7 +415,7 @@ def get_node(self, node_id: int) -> Any: """Get a specific node by ID.""" return self.request("GET", f"/nodes/{node_id}/", use_content_api=True) - def upsert_node(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_node(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: """Create or update a node (topic).""" if remote_id: data = self.request("PUT", f"/nodes/{remote_id}/", payload, use_content_api=True) @@ -339,7 +431,7 @@ def delete_node(self, node_id: int) -> None: # Content API - Metadata Configs # ========================================================================= - def list_metadata_configs(self, node_id: Optional[int] = None) -> Any: + def list_metadata_configs(self, node_id: int | None = None) -> Any: """List metadata configs, optionally filtered by node.""" if node_id: return self.request("GET", f"/nodes/{node_id}/metadata_configs/", use_content_api=True) @@ -394,9 +486,7 @@ def get_reference_formatter(self, formatter_id: int) -> Any: """Get a specific reference formatter by ID.""" return self.request("GET", f"/reference_formatters/{formatter_id}/", use_content_api=True) - def upsert_reference_formatter( - self, *, remote_id: Optional[int], payload: dict[str, Any] - ) -> int: + def upsert_reference_formatter(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: """Create or update a reference formatter.""" if remote_id: data = self.request( @@ -422,7 +512,7 @@ def get_ingestion_config(self, config_id: int) -> Any: """Get a specific ingestion configuration by ID.""" return self.request("GET", f"/ingestion-configs/{config_id}/", use_content_api=True) - def upsert_ingestion_config(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_ingestion_config(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: """Create or update an ingestion configuration.""" if remote_id: data = self.request( @@ -448,7 +538,7 @@ def get_retrieval(self, retrieval_id: int) -> Any: """Get a specific retrieval configuration by ID.""" return self.request("GET", f"/retrievals/{retrieval_id}/", use_content_api=True) - def upsert_retrieval(self, *, remote_id: Optional[int], payload: dict[str, Any]) -> int: + def upsert_retrieval(self, *, remote_id: int | None, payload: dict[str, Any]) -> int: """Create or update a retrieval configuration.""" if remote_id: data = self.request("PUT", f"/retrievals/{remote_id}/", payload, use_content_api=True) @@ -461,7 +551,7 @@ def delete_retrieval(self, retrieval_id: int) -> None: self.request("DELETE", f"/retrievals/{retrieval_id}/", use_content_api=True) def retrieve_similar_blocks( - self, retrieval_id: int, question: str, doc_type: Optional[str] = None + self, retrieval_id: int, question: str, doc_type: str | None = None ) -> Any: """Execute a semantic search using a retrieval configuration.""" payload: dict[str, Any] = {"question": question} @@ -514,13 +604,13 @@ def upload_document( name: str, node_id: int, doc_type: str = "general", - metadata: Optional[list[dict[str, Any]]] = None, - ingestion_config_id: Optional[int] = None, + metadata: list[dict[str, Any]] | None = None, + ingestion_config_id: int | None = None, pdf_parsing_mode: str = "both", chunking_mode: str = "langchain", max_size_block: int = 1500, chunk_overlap: int = 0, - separators: Optional[list[str]] = None, + separators: list[str] | None = None, ocr: bool = False, additional_prompt_instructions: str = "", assign_paths_as_metadata: bool = False, @@ -589,12 +679,12 @@ def upload_documents_bulk( file_paths: list[str | Path], node_id: int, doc_type: str = "general", - ingestion_config_id: Optional[int] = None, + ingestion_config_id: int | None = None, pdf_parsing_mode: str = "both", chunking_mode: str = "langchain", max_size_block: int = 1500, chunk_overlap: int = 0, - separators: Optional[list[str]] = None, + separators: list[str] | None = None, ocr: bool = False, additional_prompt_instructions: str = "", assign_paths_as_metadata: bool = False, diff --git a/cogsol/core/loader.py b/cogsol/core/loader.py index 34df5ed..cc42ade 100644 --- a/cogsol/core/loader.py +++ b/cogsol/core/loader.py @@ -10,7 +10,7 @@ import textwrap from enum import Enum from pathlib import Path -from typing import Any, Optional, Union, cast +from typing import Any, Union, cast from typing_extensions import TypeAlias @@ -214,7 +214,11 @@ def _extract_tool_params(tool_cls: type[BaseTool]) -> dict[str, Any]: required = meta.get("required") if required is None: required = param.default is inspect._empty - params[name] = {"description": desc, "type": typ, "required": bool(required)} + param_dict = {"description": desc, "type": typ, "required": bool(required)} + # Include 'items' for array types if specified in decorator + if typ == "array" and "items" in meta: + param_dict["items"] = meta["items"] + params[name] = param_dict return params @@ -764,7 +768,7 @@ def _collect_retrievals(project_path: Path) -> dict[str, dict[str, Any]]: fields, meta = _extract_class_fields(obj) topic_value = getattr(obj, "topic", None) - topic_cls: Optional[type[BaseTopic]] = None + topic_cls: type[BaseTopic] | None = None if isinstance(topic_value, BaseTopic): topic_cls = type(topic_value) elif isinstance(topic_value, type) and issubclass(topic_value, BaseTopic): diff --git a/cogsol/db/migrations.py b/cogsol/db/migrations.py index ec9518f..75d0e50 100644 --- a/cogsol/db/migrations.py +++ b/cogsol/db/migrations.py @@ -8,7 +8,7 @@ from collections.abc import Iterable from dataclasses import dataclass, field -from typing import Any, Optional, cast +from typing import Any, cast class Migration: @@ -28,7 +28,7 @@ def _ensure_bucket(state: dict[str, Any], entity: str) -> dict[str, Any]: class CreateDefinition: name: str fields: dict[str, Any] - meta: Optional[dict[str, Any]] = None + meta: dict[str, Any] | None = None entity: str = field(default="agents") def apply(self, state: dict[str, Any]) -> None: @@ -44,7 +44,7 @@ def __repr__(self) -> str: # pragma: no cover - used for file generation class CreateAgent(CreateDefinition): def __init__( - self, name: str, fields: dict[str, Any], meta: Optional[dict[str, Any]] = None + self, name: str, fields: dict[str, Any], meta: dict[str, Any] | None = None ) -> None: super().__init__(name=name, fields=fields, meta=meta, entity="agents") @@ -83,7 +83,7 @@ class CreateTopic(CreateDefinition): """Creates a Topic (Node) in the Content API.""" def __init__( - self, name: str, fields: dict[str, Any], meta: Optional[dict[str, Any]] = None + self, name: str, fields: dict[str, Any], meta: dict[str, Any] | None = None ) -> None: super().__init__(name=name, fields=fields, meta=meta, entity="topics") diff --git a/cogsol/management/commands/chat.py b/cogsol/management/commands/chat.py index 7951ae7..9a368cb 100644 --- a/cogsol/management/commands/chat.py +++ b/cogsol/management/commands/chat.py @@ -7,7 +7,7 @@ import textwrap from datetime import datetime from pathlib import Path -from typing import Any, Optional, cast +from typing import Any, cast from cogsol.core.api import CogSolAPIError, CogSolClient from cogsol.core.env import load_dotenv @@ -317,7 +317,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: load_dotenv(project_path / ".env") api_base = os.environ.get("COGSOL_API_BASE") - api_token = os.environ.get("COGSOL_API_TOKEN") + api_key = os.environ.get("COGSOL_API_KEY") if not api_base: print_error("COGSOL_API_BASE is required in .env to chat with CogSol.") return 1 @@ -328,7 +328,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: print_error(f"Could not resolve agent '{agent}'. Run migrate first.") return 1 - client = CogSolClient(api_base, token=api_token) + client = CogSolClient(api_base, api_key=api_key) initial_message = self._assistant_initial_message(client, assistant_id) # Start a new chat and show banner @@ -339,7 +339,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: if initial_message: print_ai_message(initial_message) - chat_id: Optional[int] = None + chat_id: int | None = None history_printed = 0 while True: @@ -426,7 +426,7 @@ def _load_remote_ids(self, project_path: Path, app: str) -> dict[str, Any]: except json.JSONDecodeError: return {} - def _resolve_agent_id(self, agent: str, remote_ids: dict[str, Any]) -> Optional[int]: + def _resolve_agent_id(self, agent: str, remote_ids: dict[str, Any]) -> int | None: # direct numeric id try: return int(agent) @@ -446,7 +446,7 @@ def _assistant_initial_message(self, client: CogSolClient, assistant_id: int) -> return value.strip() return "" - def _chat_id(self, chat_obj: Any) -> Optional[int]: + def _chat_id(self, chat_obj: Any) -> int | None: if isinstance(chat_obj, dict): value = chat_obj.get("id") if isinstance(value, int): diff --git a/cogsol/management/commands/importagent.py b/cogsol/management/commands/importagent.py index 813b59c..414c01d 100644 --- a/cogsol/management/commands/importagent.py +++ b/cogsol/management/commands/importagent.py @@ -3,7 +3,7 @@ import json import re from pathlib import Path -from typing import Any, Optional, cast +from typing import Any, cast from cogsol.core.api import CogSolAPIError, CogSolClient from cogsol.core.env import load_dotenv @@ -170,7 +170,7 @@ def _retrieval_tool_class_name(tool: dict[str, Any]) -> str: return base_name if base_name.endswith("Search") else base_name + "Search" -def _retrieval_tool_class_from_api(tool: dict[str, Any], retrieval_name: Optional[str]) -> str: +def _retrieval_tool_class_from_api(tool: dict[str, Any], retrieval_name: str | None) -> str: name = tool.get("name") or "Search" class_name = _retrieval_tool_class_name(tool) description = tool.get("description") or f"Retrieval tool {name}" @@ -290,13 +290,13 @@ def handle(self, project_path: Path | None, **options: Any) -> int: import os api_base = os.environ.get("COGSOL_API_BASE") - api_token = os.environ.get("COGSOL_API_TOKEN") + api_key = os.environ.get("COGSOL_API_KEY") content_base = os.environ.get("COGSOL_CONTENT_API_BASE") or api_base if not api_base: print("COGSOL_API_BASE is required in .env to import.") return 1 - client = CogSolClient(api_base, token=api_token, content_base_url=content_base) + client = CogSolClient(api_base, api_key=api_key, content_base_url=content_base) try: assistant = client.get_assistant(assistant_id) faqs = client.list_common_questions(assistant_id) or [] @@ -375,9 +375,9 @@ class Meta: scripts_by_id: dict[int, dict[str, Any]] = {} retrieval_tools: list[dict[str, Any]] = [] retrieval_tools_by_id: dict[int, dict[str, Any]] = {} - retrieval_cache: dict[int, Optional[str]] = {} + retrieval_cache: dict[int, str | None] = {} - def _resolve_retrieval_name(retrieval_id: Optional[int]) -> Optional[str]: + def _resolve_retrieval_name(retrieval_id: int | None) -> str | None: if not retrieval_id: return None if retrieval_id in retrieval_cache: @@ -454,20 +454,20 @@ def _resolve_retrieval_name(retrieval_id: Optional[int]) -> Optional[str]: has_retrieval_tools = True # Update tools list in agent.py - def class_name_for_script(script_id: int) -> Optional[str]: + def class_name_for_script(script_id: int) -> str | None: script = scripts_by_id.get(int(script_id)) if not script: return None base_name = _safe_class_name(script.get("name") or "Tool", "Imported") return base_name if base_name.endswith("Tool") else base_name + "Tool" - def class_name_for_retrieval_tool(tool_id: int) -> Optional[str]: + def class_name_for_retrieval_tool(tool_id: int) -> str | None: tool = retrieval_tools_by_id.get(int(tool_id)) if not tool: return None return _retrieval_tool_class_name(tool) - def _tool_class_for_id(tool_id: int) -> Optional[str]: + def _tool_class_for_id(tool_id: int) -> str | None: return class_name_for_script(tool_id) or class_name_for_retrieval_tool(tool_id) tool_class_names = [n for n in (_tool_class_for_id(sid) for sid in tools_ids) if n] @@ -537,7 +537,7 @@ def _tool_class_for_id(tool_id: int) -> Optional[str]: f" migrations.CreateRetrievalTool(name={tname!r}, fields={fields!r})," ) - def _tool_name_for_id(tool_id: int) -> Optional[str]: + def _tool_name_for_id(tool_id: int) -> str | None: script = scripts_by_id.get(int(tool_id)) if script: return script.get("name") @@ -703,7 +703,7 @@ def _get_node(node_id: int) -> dict[str, Any]: return node return {} - def _node_parent_id(node: dict[str, Any]) -> Optional[int]: + def _node_parent_id(node: dict[str, Any]) -> int | None: parent = node.get("parent") if isinstance(parent, dict): return parent.get("id") @@ -713,7 +713,7 @@ def _node_parent_id(node: dict[str, Any]) -> Optional[int]: def _node_chain(node_id: int) -> list[dict[str, Any]]: chain: list[dict[str, Any]] = [] - current: Optional[int] = node_id + current: int | None = node_id while current is not None: node = _get_node(current) if not node: diff --git a/cogsol/management/commands/ingest.py b/cogsol/management/commands/ingest.py index 3268268..f77c6c4 100644 --- a/cogsol/management/commands/ingest.py +++ b/cogsol/management/commands/ingest.py @@ -8,7 +8,7 @@ import inspect import sys from pathlib import Path -from typing import Any, Optional +from typing import Any from cogsol.content import BaseIngestionConfig, DocType from cogsol.core.api import CogSolClient @@ -56,12 +56,12 @@ def get_client(project_path: Path) -> CogSolClient: api_base = api_base or os.environ.get("COGSOL_API_BASE", "http://localhost:8000") content_base = content_base or os.environ.get("COGSOL_CONTENT_API_BASE", api_base) - token = os.environ.get("COGSOL_API_TOKEN") + token = os.environ.get("COGSOL_API_KEY") - return CogSolClient(base_url=api_base, token=token, content_base_url=content_base) + return CogSolClient(base_url=api_base, api_key=token, content_base_url=content_base) -def load_ingestion_config(project_path: Path, config_name: str) -> Optional[BaseIngestionConfig]: +def load_ingestion_config(project_path: Path, config_name: str) -> BaseIngestionConfig | None: """Load an ingestion config by name from data/ingestion.py.""" sys.path.insert(0, str(project_path)) try: @@ -89,7 +89,7 @@ def load_ingestion_config(project_path: Path, config_name: str) -> Optional[Base pass -def find_topic_node_id(client: CogSolClient, topic_path: str) -> Optional[int]: +def find_topic_node_id(client: CogSolClient, topic_path: str) -> int | None: """ Find the node ID for a topic given its path (e.g., 'parent/child/topic'). @@ -131,7 +131,7 @@ def find_topic_node_id(client: CogSolClient, topic_path: str) -> Optional[int]: return None -def collect_files(paths: list[str], pattern: Optional[str] = None) -> list[Path]: +def collect_files(paths: list[str], pattern: str | None = None) -> list[Path]: """Collect files from paths, expanding globs and directories.""" files = [] for path_str in paths: @@ -307,7 +307,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: return 1 # Look up ingestion config if provided - ing_config: Optional[BaseIngestionConfig] = None + ing_config: BaseIngestionConfig | None = None if ingestion_config: ing_config = load_ingestion_config(project_path, ingestion_config) if ing_config is None: diff --git a/cogsol/management/commands/migrate.py b/cogsol/management/commands/migrate.py index 5ee6a48..a876ce0 100644 --- a/cogsol/management/commands/migrate.py +++ b/cogsol/management/commands/migrate.py @@ -34,7 +34,7 @@ def _normalize_code(code: Any) -> str: return textwrap.dedent(code).rstrip() -def sub_slug(cls: Optional[type]) -> Optional[str]: +def sub_slug(cls: type | None) -> str | None: if cls and hasattr(cls, "__module__"): parts = cls.__module__.split(".") if len(parts) >= 2: @@ -60,7 +60,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: load_dotenv(project_path / ".env") api_base = self._env("COGSOL_API_BASE") - api_token = self._env("COGSOL_API_TOKEN", required=False) + api_key = self._env("COGSOL_API_KEY", required=False) content_base = self._env("COGSOL_CONTENT_API_BASE", required=False) or api_base if not api_base: print("COGSOL_API_BASE is required in .env to run migrations against CogSol APIs.") @@ -125,7 +125,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: class_map = collect_content_classes(project_path, app_name) remote_ids = self._sync_content_with_api( api_base=content_base or api_base, - api_token=api_token, + api_key=api_key, state=temp_state, remote_ids=remote_ids, class_map=class_map, @@ -136,7 +136,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: class_map = collect_classes(project_path, app_name) remote_ids = self._sync_with_api( api_base=api_base, - api_token=api_token, + api_key=api_key, state=temp_state, remote_ids=remote_ids, class_map=class_map, @@ -157,7 +157,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: return exit_code # ------------------------------------------------------------------ helpers - def _env(self, key: str, required: bool = True) -> Optional[str]: + def _env(self, key: str, required: bool = True) -> str | None: import os value = os.environ.get(key) @@ -231,16 +231,16 @@ def _sync_content_with_api( self, *, api_base: str, - api_token: Optional[str], + api_key: str | None, state: dict[str, Any], remote_ids: dict[str, Any], class_map: dict[str, dict[str, type]], project_path: Path, - touched: Optional[dict[str, set[str]]] = None, + touched: dict[str, set[str]] | None = None, ) -> dict[str, Any]: """Sync Content API entities (topics, formatters, retrievals) with the API.""" - client = CogSolClient(api_base, token=api_token, content_base_url=api_base) - created: list[tuple[str, Optional[int], int]] = [] + client = CogSolClient(api_base, api_key=api_key, content_base_url=api_base) + created: list[tuple[str, int | None, int]] = [] new_remote = copy.deepcopy(remote_ids) try: @@ -486,16 +486,16 @@ def _sync_with_api( self, *, api_base: str, - api_token: Optional[str], + api_key: str | None, state: dict[str, Any], remote_ids: dict[str, Any], class_map: dict[str, dict[str, type]], project_path: Path, app: str, - touched: Optional[dict[str, set[str]]] = None, + touched: dict[str, set[str]] | None = None, ) -> dict[str, Any]: - client = CogSolClient(api_base, token=api_token) - created: list[tuple[str, Optional[int], int]] = [] + client = CogSolClient(api_base, api_key=api_key) + created: list[tuple[str, int | None, int]] = [] new_remote = copy.deepcopy(remote_ids) try: @@ -670,7 +670,7 @@ def _tool_payload( self, tool_name: str, definition: dict[str, Any], - cls: Optional[type[BaseTool]], + cls: type[BaseTool] | None, ) -> dict[str, Any]: params = [] if cls is not None: @@ -679,14 +679,16 @@ def _tool_payload( param_def = definition.get("fields", {}).get("parameters", {}) if definition else {} for name, meta in (param_def or {}).items(): meta = meta or {} - params.append( - { - "name": name, - "description": meta.get("description") or name, - "type": meta.get("type") or "string", - "required": bool(meta.get("required", True)), - } - ) + param_entry = { + "name": name, + "description": meta.get("description") or name, + "type": meta.get("type") or "string", + "required": bool(meta.get("required", True)), + } + # Include 'items' for array types if specified + if param_entry["type"] == "array" and "items" in meta: + param_entry["items"] = meta["items"] + params.append(param_entry) description = ( (definition.get("fields", {}) or {}).get("description") if definition else None @@ -713,7 +715,7 @@ def _retrieval_tool_payload( *, tool_name: str, definition: dict[str, Any], - cls: Optional[type], + cls: type | None, project_path: Path, ) -> dict[str, Any]: fields = definition.get("fields", {}) if definition else {} @@ -774,11 +776,11 @@ def _assistant_payload( *, agent_name: str, definition: dict[str, Any], - cls: Optional[type], + cls: type | None, remote_ids: dict[str, Any], project_path: Path, app: str, - slug: Optional[str] = None, + slug: str | None = None, ) -> dict[str, Any]: fields = definition.get("fields", {}) if definition else {} meta = definition.get("meta", {}) if definition else {} @@ -842,7 +844,7 @@ def _prompt_text(value: Any) -> str: tools = getattr(cls, "tools", []) if cls else [] pretools = getattr(cls, "pretools", []) if cls else [] - def _resolve_tool_id(t) -> Optional[int]: + def _resolve_tool_id(t) -> int | None: candidates = [ getattr(t, "name", None), _tool_key(t), @@ -981,7 +983,7 @@ def _replace_self_calls(self, code: str, helper_names: list[str]) -> str: rewritten = re.sub(rf"\bself\.{name}\b", name, rewritten) return rewritten - def _tool_script_from_class(self, cls: Optional[type[BaseTool]]) -> str: + def _tool_script_from_class(self, cls: type[BaseTool] | None) -> str: if cls is None: return "" try: diff --git a/cogsol/management/commands/startproject.py b/cogsol/management/commands/startproject.py index 7c1723f..865315f 100644 --- a/cogsol/management/commands/startproject.py +++ b/cogsol/management/commands/startproject.py @@ -194,7 +194,7 @@ def handle(self, project_path: Path | None, **options: Any) -> int: "data/retrievals.py": DATA_RETRIEVALS_PY, "data/migrations/__init__.py": "", "README.md": README.format(project_name=name), - ".env.example": "COGSOL_ENV=local\nCOGSOL_API_BASE=http://localhost:8000\nCOGSOL_CONTENT_API_BASE=http://localhost:8001\n# Optional: COGSOL_API_TOKEN=your-token\n", + ".env.example": "COGSOL_ENV=local\nCOGSOL_API_BASE=http://localhost:8000\nCOGSOL_CONTENT_API_BASE=http://localhost:8001\n# Optional: COGSOL_API_KEY=your-api-key\n# Optional: Azure AD B2C client credentials for JWT\n# If not provided, the Auth will be skipped\n# COGSOL_AUTH_CLIENT_ID=you-client-id\n# COGSOL_AUTH_SECRET=your-secret\n", } for relative_path, content in files.items(): diff --git a/cogsol/management/commands/topics.py b/cogsol/management/commands/topics.py index e41d30f..87e3810 100644 --- a/cogsol/management/commands/topics.py +++ b/cogsol/management/commands/topics.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Optional +from typing import Any from cogsol.core.api import CogSolClient from cogsol.core.env import load_dotenv @@ -32,12 +32,12 @@ def get_client(project_path: Path) -> CogSolClient: api_base = api_base or os.environ.get("COGSOL_API_BASE", "http://localhost:8000") content_base = content_base or os.environ.get("COGSOL_CONTENT_API_BASE", api_base) - token = os.environ.get("COGSOL_API_TOKEN") + api_key = os.environ.get("COGSOL_API_KEY") - return CogSolClient(base_url=api_base, token=token, content_base_url=content_base) + return CogSolClient(base_url=api_base, api_key=api_key, content_base_url=content_base) -def build_tree(nodes: list[dict], parent_id: Optional[int] = None, prefix: str = "") -> list[str]: +def build_tree(nodes: list[dict], parent_id: int | None = None, prefix: str = "") -> list[str]: """Build a tree representation of nodes.""" lines = [] children = [n for n in nodes if n.get("parent") == parent_id] diff --git a/cogsol/prompts.py b/cogsol/prompts.py index d95f5ad..b8da9a8 100644 --- a/cogsol/prompts.py +++ b/cogsol/prompts.py @@ -5,11 +5,10 @@ from __future__ import annotations from pathlib import Path -from typing import Optional class Prompt: - def __init__(self, path: str, base_dir: Optional[str] = None) -> None: + def __init__(self, path: str, base_dir: str | None = None) -> None: self.path = path self.base_dir = base_dir @@ -28,7 +27,7 @@ def load(path: str) -> Prompt: import inspect caller_frame = inspect.currentframe() - base_dir: Optional[str] = None + base_dir: str | None = None if caller_frame and caller_frame.f_back: caller_file = caller_frame.f_back.f_code.co_filename try: diff --git a/cogsol/tools/__init__.py b/cogsol/tools/__init__.py index f088484..78ce3ff 100644 --- a/cogsol/tools/__init__.py +++ b/cogsol/tools/__init__.py @@ -5,15 +5,15 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any class BaseTool: - name: Optional[str] = None - description: Optional[str] = None + name: str | None = None + description: str | None = None parameters: dict[str, Any] = {} - def __init__(self, name: Optional[str] = None, description: Optional[str] = None): + def __init__(self, name: str | None = None, description: str | None = None): if name: self.name = name if description: @@ -31,40 +31,40 @@ def __repr__(self) -> str: class BaseLesson: - name: Optional[str] = None - content: Optional[str] = None + name: str | None = None + content: str | None = None def __repr__(self) -> str: return f"" class BaseFAQ: - question: Optional[str] = None - answer: Optional[str] = None + question: str | None = None + answer: str | None = None def __repr__(self) -> str: return f"" class BaseFixedResponse: - key: Optional[str] = None - response: Optional[str] = None + key: str | None = None + response: str | None = None def __repr__(self) -> str: return f"" class BaseRetrievalTool: - name: Optional[str] = None - description: Optional[str] = None + name: str | None = None + description: str | None = None parameters: list[dict[str, Any]] = [] - retrieval: Optional[str] = None + retrieval: str | None = None show_tool_message: bool = False show_assistant_message: bool = False edit_available: bool = True answer: bool = True - def __init__(self, name: Optional[str] = None, description: Optional[str] = None): + def __init__(self, name: str | None = None, description: str | None = None): if name: self.name = name if description: diff --git a/docs/commands.md b/docs/commands.md index fde3d74..10da148 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -150,7 +150,10 @@ Contains commented examples of retrieval configurations. COGSOL_ENV=local COGSOL_API_BASE=http://localhost:8000 COGSOL_CONTENT_API_BASE=http://localhost:8001 -# Optional: COGSOL_API_TOKEN=your-token +# Optional: COGSOL_API_KEY=your-api-key +# Optional: Azure AD B2C client credentials for JWT +# COGSOL_AUTH_CLIENT_ID=your-client-id +# COGSOL_AUTH_SECRET=your-client-secret ``` #### Example Usage @@ -483,7 +486,9 @@ python manage.py migrate [app] ```env COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ # Required COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ # Required for data app -COGSOL_API_TOKEN=your-token # Optional, but recommended +COGSOL_API_KEY=your-api-key # Optional, but recommended +COGSOL_AUTH_CLIENT_ID=your-client-id +COGSOL_AUTH_SECRET=your-client-secret ``` #### How It Works @@ -730,7 +735,8 @@ python manage.py importagent [app] ```env COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ # Required -COGSOL_API_TOKEN=your-token # Optional +COGSOL_API_KEY=your-api-key # Optional + COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ # Required if importing retrievals ``` @@ -812,7 +818,7 @@ python manage.py chat --agent [app] ```env COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ # Required -COGSOL_API_TOKEN=your-token # Optional +COGSOL_API_KEY=your-api-key # Optional ``` #### Agent Resolution @@ -891,7 +897,7 @@ COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ # Optional: API authentication token -COGSOL_API_TOKEN=sk-your-api-token +COGSOL_API_KEY=sk-your-api-key # Optional: Environment identifier COGSOL_ENV=production @@ -916,7 +922,7 @@ def load_dotenv(dotenv_path: Path) -> None: |----------|--------------|-------------| | `COGSOL_API_BASE` | `migrate`, `chat`, `importagent` | Base URL for CogSol API | | `COGSOL_CONTENT_API_BASE` | `migrate`, `ingest`, `topics`, `importagent` | Base URL for the Content API (defaults to `COGSOL_API_BASE`) | -| `COGSOL_API_TOKEN` | - | API authentication (via `x-api-key` header) | +| `COGSOL_API_KEY` | - | API authentication (via `x-api-key` header) | | `COGSOL_ENV` | - | Environment name (informational) | --- @@ -979,12 +985,13 @@ python -c "from agents.myagent.agent import *" #### "API error: 401 Unauthorized" -Add or update your API token: +Add or update your API Key: ```env -COGSOL_API_TOKEN=your-valid-token +COGSOL_API_KEY=your-valid-api-key ``` +If it doesn't work and you have the token auth configured, check if your credentials are valid. ### Debug Tips 1. **Check state files**: Look at `agents/migrations/.state.json` for current mappings diff --git a/docs/getting-started.md b/docs/getting-started.md index 866f7ef..98ef069 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -89,7 +89,9 @@ Update `.env` with your CogSol API credentials: COGSOL_ENV=development COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ -COGSOL_API_TOKEN=your-api-token-here +COGSOL_API_KEY=your-api-key-here +COGSOL_AUTH_CLIENT_ID=your-client-id +COGSOL_AUTH_SECRET=your-client-secret ``` ### Step 3: Verify Project Setup @@ -669,7 +671,9 @@ Verify your `.env` file has valid credentials: ```env COGSOL_API_BASE=https://api.cogsol.ai/cognitive/ COGSOL_CONTENT_API_BASE=https://api.cogsol.ai/content/ -COGSOL_API_TOKEN=sk-your-valid-token +COGSOL_API_KEY=sk-your-valid-api-key +COGSOL_AUTH_CLIENT_ID=your-client-id +COGSOL_AUTH_SECRET=your-client-secret ``` ### Step 2: Apply Migrations @@ -829,7 +833,8 @@ python -c "from agents.customersupport.agent import *" ### "API error: 401 Unauthorized" -Verify your `COGSOL_API_TOKEN` is correct and not expired. +Verify if `COGSOL_API_KEY`, `COGSOL_AUTH_CLIENT_ID` +`COGSOL_AUTH_SECRET` are correct and not expired. --- diff --git a/pyproject.toml b/pyproject.toml index c00b9d5..456ad9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,10 @@ authors = [{ name = "Cognitive Solutions" }] readme = "README.md" license = { text = "MIT" } requires-python = ">=3.9" -dependencies = [] +dependencies = [ + "msal>=1.27", + "PyJWT>=2.8", +] keywords = ["ai", "agents", "llm", "framework", "assistant", "chatbot"] classifiers = [ "Development Status :: 3 - Alpha", @@ -59,7 +62,7 @@ target-version = "py39" [tool.ruff.lint] select = ["E", "F", "W", "I", "UP", "B", "C4"] -ignore = ["E501","UP045"] +ignore = ["E501", "UP007"] [tool.mypy] python_version = "3.9"