diff --git a/navi/api/routes/auth.py b/navi/api/routes/auth.py index e1b45f8..fe28a5e 100644 --- a/navi/api/routes/auth.py +++ b/navi/api/routes/auth.py @@ -95,19 +95,40 @@ store = get_session_store() pool = await store._get_pool() import json + profile = auth_user.profile async with pool.acquire() as conn: await conn.execute( - """INSERT INTO navi_users (id, email, display_name, role, permissions, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $6) + """INSERT INTO navi_users ( + id, email, display_name, username, first_name, last_name, + phone, birth_date, country, city, locale, + role, permissions, created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $14) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, display_name = EXCLUDED.display_name, + username = EXCLUDED.username, + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + phone = EXCLUDED.phone, + birth_date = EXCLUDED.birth_date, + country = EXCLUDED.country, + city = EXCLUDED.city, + locale = EXCLUDED.locale, role = EXCLUDED.role, permissions = EXCLUDED.permissions, updated_at = EXCLUDED.updated_at""", auth_user.user_id, auth_user.email, - auth_user.profile.get("display_name") or auth_user.email, + profile.get("display_name") or auth_user.email, + profile.get("username"), + profile.get("first_name"), + profile.get("last_name"), + profile.get("phone"), + profile.get("birth_date"), + profile.get("country"), + profile.get("city"), + profile.get("locale"), role, json.dumps(permissions), datetime.now(timezone.utc), @@ -197,6 +218,14 @@ "id": user.id, "email": user.email, "display_name": user.display_name, + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "phone": user.phone, + "birth_date": user.birth_date, + "country": user.country, + "city": user.city, + "locale": user.locale, "avatar_url": user.avatar_url, "role": user.role, "permissions": user.permissions, diff --git a/navi/api/routes/messages.py b/navi/api/routes/messages.py index 76ef0eb..1d0b446 100644 --- a/navi/api/routes/messages.py +++ b/navi/api/routes/messages.py @@ -32,9 +32,10 @@ check_session_access(session, user) # Set user context for tool sandboxing - from navi.tools.base import current_user_id as _uid_var, current_user_role as _role_var + from navi.tools.base import current_user_id as _uid_var, current_user_role as _role_var, current_user_info as _uinfo_var _uid_var.set(user.id) _role_var.set(user.role) + _uinfo_var.set(user.model_dump(mode="json")) try: reply = await agent.run(session_id, body.content) diff --git a/navi/api/websocket.py b/navi/api/websocket.py index ee06c44..efaa068 100644 --- a/navi/api/websocket.py +++ b/navi/api/websocket.py @@ -362,13 +362,15 @@ _runs[session_id] = run # Set user context for tool sandboxing (inherited by the agent task) - from navi.tools.base import current_user_id as _uid_var, current_user_role as _role_var + from navi.tools.base import current_user_id as _uid_var, current_user_role as _role_var, current_user_info as _uinfo_var if user is not None: _uid_var.set(user.id) _role_var.set(user.role) + _uinfo_var.set(user.model_dump(mode="json")) else: _uid_var.set(None) _role_var.set("user") + _uinfo_var.set(None) run.task = asyncio.create_task( _run_agent(run, agent, session_id, user_content, raw_images, original_content) diff --git a/navi/auth/__init__.py b/navi/auth/__init__.py index 52f5209..f664181 100644 --- a/navi/auth/__init__.py +++ b/navi/auth/__init__.py @@ -11,6 +11,14 @@ id: str email: str display_name: str | None = None + username: str | None = None + first_name: str | None = None + last_name: str | None = None + phone: str | None = None + birth_date: str | None = None + country: str | None = None + city: str | None = None + locale: str | None = None avatar_url: str | None = None role: str = "user" # "user" | "admin" permissions: list[str] = [] diff --git a/navi/auth/_ddl.py b/navi/auth/_ddl.py index 8430136..35a1a29 100644 --- a/navi/auth/_ddl.py +++ b/navi/auth/_ddl.py @@ -7,10 +7,18 @@ id TEXT PRIMARY KEY, email TEXT NOT NULL, display_name TEXT, - role TEXT NOT NULL DEFAULT 'user', - permissions TEXT NOT NULL DEFAULT '[]', - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + username TEXT, + first_name TEXT, + last_name TEXT, + phone TEXT, + birth_date TEXT, + country TEXT, + city TEXT, + locale TEXT, + role TEXT NOT NULL DEFAULT 'user', + permissions TEXT NOT NULL DEFAULT '[]', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS user_auth_sessions ( diff --git a/navi/auth/deps.py b/navi/auth/deps.py index f485008..11c4a7c 100644 --- a/navi/auth/deps.py +++ b/navi/auth/deps.py @@ -109,12 +109,33 @@ break # Upsert into navi_users - await _upsert_navi_user(store, auth_user.user_id, auth_user.email, auth_user.profile.get("display_name"), role, permissions) + profile = auth_user.profile + await _upsert_navi_user( + store, auth_user.user_id, auth_user.email, + profile.get("display_name"), role, permissions, + username=profile.get("username"), + first_name=profile.get("first_name"), + last_name=profile.get("last_name"), + phone=profile.get("phone"), + birth_date=profile.get("birth_date"), + country=profile.get("country"), + city=profile.get("city"), + locale=profile.get("locale"), + ) + profile = auth_user.profile user = User( id=auth_user.user_id, email=auth_user.email, - display_name=auth_user.profile.get("display_name") or auth_user.email, + display_name=profile.get("display_name") or auth_user.email, + username=profile.get("username"), + first_name=profile.get("first_name"), + last_name=profile.get("last_name"), + phone=profile.get("phone"), + birth_date=profile.get("birth_date"), + country=profile.get("country"), + city=profile.get("city"), + locale=profile.get("locale"), avatar_url=auth_user.avatar_url, role=role, permissions=permissions, @@ -193,21 +214,38 @@ async def _upsert_navi_user( - store, user_id: str, email: str, display_name: str | None, role: str, permissions: list[str] + store, user_id: str, email: str, display_name: str | None, role: str, permissions: list[str], + username: str | None = None, first_name: str | None = None, last_name: str | None = None, + phone: str | None = None, birth_date: str | None = None, country: str | None = None, + city: str | None = None, locale: str | None = None, ) -> None: pool = await store._get_pool() import json async with pool.acquire() as conn: await conn.execute( - """INSERT INTO navi_users (id, email, display_name, role, permissions, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $6) + """INSERT INTO navi_users ( + id, email, display_name, username, first_name, last_name, + phone, birth_date, country, city, locale, + role, permissions, created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $14) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, display_name = EXCLUDED.display_name, + username = EXCLUDED.username, + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + phone = EXCLUDED.phone, + birth_date = EXCLUDED.birth_date, + country = EXCLUDED.country, + city = EXCLUDED.city, + locale = EXCLUDED.locale, role = EXCLUDED.role, permissions = EXCLUDED.permissions, updated_at = EXCLUDED.updated_at""", - user_id, email, display_name or email, role, json.dumps(permissions), datetime.now(timezone.utc), + user_id, email, display_name or email, username, first_name, last_name, + phone, birth_date, country, city, locale, + role, json.dumps(permissions), datetime.now(timezone.utc), ) diff --git a/navi/core/agent.py b/navi/core/agent.py index f475d27..dcae151 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -245,15 +245,13 @@ await self._sessions.save(session) ctx_injections = await self._ctx_builder._collect_context_injections(profile) - user_id = session.user_id - user_role = current_user_role.get() for iteration in range(profile.max_iterations): log.debug("agent.iteration", session_id=session_id, iteration=iteration) response = await llm.complete( self._ctx_builder.build(session.context, profile, mem, iteration=iteration, max_iterations=profile.max_iterations, extra_system=ctx_injections, - session_id=session_id, user_id=user_id, user_role=user_role), + session_id=session_id), tools=tool_schemas if tools else None, temperature=profile.temperature, model=profile.model, @@ -331,11 +329,13 @@ current_model as _model_var, current_user_id as _uid_var, current_user_role as _role_var, + current_user_info as _uinfo_var, ) _prev_sid = _sid_var.get(None) _prev_model = _model_var.get(None) _prev_uid = _uid_var.get(None) _prev_role = _role_var.get() + _prev_uinfo = _uinfo_var.get(None) subagent_run_id = f"subagent_{_uuid.uuid4().hex[:12]}" tool_session_id = parent_session_id or subagent_run_id _sid_var.set(tool_session_id) @@ -358,9 +358,11 @@ if user_id is not None: _uid_var.set(user_id) _role_var.set(_prev_role or "user") + _uinfo_var.set(_prev_uinfo) else: _uid_var.set(None) _role_var.set("user") + _uinfo_var.set(None) mem = await self._ctx_builder._memory_msg(user_id=user_id) # Build subagent system prompt — completely separate from the parent's system prompt. @@ -594,6 +596,7 @@ _model_var.set(_prev_model) _uid_var.set(_prev_uid) _role_var.set(_prev_role) + _uinfo_var.set(_prev_uinfo) async def run_stream( self, @@ -711,8 +714,6 @@ yield StreamStopped() return - user_id = session.user_id - user_role = current_user_role.get() if settings.context_compression_enabled and iteration > 0: preflight_ctx = self._ctx_builder.build( session.context, @@ -720,8 +721,6 @@ mem, extra_system=ctx_injections, session_id=session_id, - user_id=user_id, - user_role=user_role, ) estimated_tokens = self._estimate_context_tokens(preflight_ctx) if should_compress( @@ -750,7 +749,7 @@ built_ctx = self._ctx_builder.build(session.context, profile, mem, iteration=iteration, max_iterations=profile.max_iterations, extra_system=ctx_injections, - session_id=session_id, user_id=user_id, user_role=user_role) + session_id=session_id) if ( profile.goal_anchoring_enabled diff --git a/navi/core/context_builder.py b/navi/core/context_builder.py index ac441de..6a42ec9 100644 --- a/navi/core/context_builder.py +++ b/navi/core/context_builder.py @@ -118,8 +118,11 @@ lines.append("Before final response, update todo for every completed step, including the final one.") return Message(role="system", content="\n".join(lines)) - def _security_policy_msg(self, user_id: str | None, role: str) -> Message | None: + def _security_policy_msg(self) -> Message | None: """Build a dynamic security policy system message based on user role.""" + from navi.tools.base import current_user_id as _uid_var, current_user_role as _role_var + user_id = _uid_var.get(None) + role = _role_var.get() if role == "admin": return Message( role="system", @@ -150,6 +153,42 @@ # Legacy / single-user mode — no policy injected return None + def _user_context_msg(self) -> Message | None: + """Build a [User context] system message from current_user_info ContextVar.""" + from navi.tools.base import current_user_info as _uinfo_var, current_user_role as _role_var + info = _uinfo_var.get(None) + if not info: + return None + lines = ["[User context]"] + if info.get("display_name"): + lines.append(f"Name: {info['display_name']}") + if info.get("username"): + lines.append(f"Username: {info['username']}") + if info.get("first_name") or info.get("last_name"): + parts = [] + if info.get("first_name"): + parts.append(info["first_name"]) + if info.get("last_name"): + parts.append(info["last_name"]) + lines.append(f"Full name: {' '.join(parts)}") + if info.get("email"): + lines.append(f"Email: {info['email']}") + if info.get("phone"): + lines.append(f"Phone: {info['phone']}") + if info.get("birth_date"): + lines.append(f"Birth date: {info['birth_date']}") + if info.get("country") or info.get("city"): + parts = [] + if info.get("city"): + parts.append(info["city"]) + if info.get("country"): + parts.append(info["country"]) + lines.append(f"Location: {', '.join(parts)}") + if info.get("locale"): + lines.append(f"Locale: {info['locale']}") + lines.append(f"Role: {_role_var.get()}") + return Message(role="system", content="\n".join(lines)) + def build( self, session_context: list[Message], @@ -159,8 +198,6 @@ max_iterations: int | None = None, extra_system: list[Message] | None = None, session_id: str | None = None, - user_id: str | None = None, - user_role: str = "user", ) -> list[Message]: system_prompt = self.build_system_prompt(profile) if session_id: @@ -177,8 +214,13 @@ if mem: result.append(mem) + # Inject user profile context for multi-user mode + user_ctx = self._user_context_msg() + if user_ctx: + result.append(user_ctx) + # Inject security policy for multi-user mode - policy = self._security_policy_msg(user_id, user_role) + policy = self._security_policy_msg() if policy: result.append(policy) diff --git a/navi/tools/base.py b/navi/tools/base.py index 0a2e0d3..8bc54fc 100644 --- a/navi/tools/base.py +++ b/navi/tools/base.py @@ -38,6 +38,10 @@ # Set by run_stream() alongside current_user_id. Admins bypass sandbox restrictions. current_user_role: ContextVar[str] = ContextVar("current_user_role", default="user") +# Set by run_stream() / messages endpoint to expose the full user profile to +# ContextBuilder so the LLM receives [User context] with name, email, locale, etc. +current_user_info: ContextVar[dict | None] = ContextVar("current_user_info", default=None) + @dataclass class ToolResult: